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();
|
`.trim();
|
||||||
|
|
||||||
// Règles détaillées injectées dans le prompt utilisateur pour guider la décision
|
// Règles détaillées injectées dans le prompt utilisateur pour guider la décision
|
||||||
const policy = `
|
const decisionPolicy = `
|
||||||
RUBRIQUE DE DÉCISION (résumée):
|
<POLITIQUE>
|
||||||
|
<SECTION id="A">
|
||||||
A. QUAND BLOQUER (blocked = true) — cas graves et clairs (pas ambigus):
|
<TITRE>QUAND BLOQUER (blocked = true) — cas graves et clairs (pas ambigus)</TITRE>
|
||||||
1) Sexualité impliquant des mineurs (référence explicite, érotisation, incitation).
|
<CAS>Sexualité impliquant des mineurs (référence explicite, érotisation, incitation).</CAS>
|
||||||
2) Apologie explicite et sérieuse de terrorisme/extrémisme réel, ou appel à rejoindre/soutenir.
|
<CAS>Apologie explicite et sérieuse de terrorisme ou extrémisme réel, ou appel à rejoindre/soutenir.</CAS>
|
||||||
3) Appels explicites à la violence ou à un préjudice grave contre une personne/groupe réel.
|
<CAS>Appels explicites à la violence ou à un préjudice grave contre une personne ou un groupe réel.</CAS>
|
||||||
4) Déshumanisation évidente et sérieuse de groupes protégés couplée à menace/incitation.
|
<CAS>Déshumanisation évidente d'un groupe protégé accompagnée d'une menace ou d'une incitation.</CAS>
|
||||||
5) Instruction ou encouragement clair à commettre des crimes violents.
|
<CAS>Instruction ou encouragement clair à commettre des crimes violents.</CAS>
|
||||||
6) Nazisme/fascisme réel promu (praise/advocacy) ou symboles glorifiés sans critique.
|
<CAS>Nazisme ou fascisme réel promu sans distance critique.</CAS>
|
||||||
7) Partage de contenu sexuel non consenti réel (ex: doxxing sexuel, vengeance) ou exploitation.
|
<CAS>Partage de contenu sexuel non consenti ou exploitation sexuelle réelle.</CAS>
|
||||||
|
</SECTION>
|
||||||
B. QUAND NE PAS BLOQUER (blocked = false, mais flagged peut être true):
|
<SECTION id="B">
|
||||||
- Œuvres qui mettent en scène un personnage toxique pour le critiquer, le montrer ou le contextualiser.
|
<TITRE>QUAND NE PAS BLOQUER (blocked = false, flagged possible)</TITRE>
|
||||||
- Citations, descriptions historiques, satire, ironie, mises en garde, ou re-appropriation par les communautés visées.
|
<CAS>Œuvres montrant un personnage toxique pour le critiquer, le contextualiser ou le dénoncer.</CAS>
|
||||||
- Métaphores violentes ou langage dur SANS cible réelle ni intention d'incitation.
|
<CAS>Citations, descriptions historiques, satire, ironie ou réappropriation par les communautés visées.</CAS>
|
||||||
- Références à des thèmes sombres dans un but narratif/éducatif/critique, sans promotion réelle.
|
<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>
|
||||||
C. SCORE ET SEVERITY:
|
</SECTION>
|
||||||
- Calibre "score" selon l'impact potentiel et la clarté du tort.
|
<SECTION id="C">
|
||||||
- Indice: low≈0.1–0.3, medium≈0.3–0.6, high≈0.6–0.85, critical>0.85.
|
<TITRE>CALIBRAGE DU SCORE ET DES SEVERITY</TITRE>
|
||||||
- Utilise severity par extrait pour refléter le pire passage.
|
<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>
|
||||||
D. RELECTURE DE CONTEXTE:
|
</SECTION>
|
||||||
- Demande-toi si le passage est: (i) narration/roleplay/critique —> plutôt allow; (ii) injonction réelle —> plutôt block.
|
<SECTION id="D">
|
||||||
- Si ambigu OU probablement artistique, NE PAS bloquer: flagged=true si nécessaire, blocked=false.
|
<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();
|
`.trim();
|
||||||
|
|
||||||
const userPrompt = `
|
const userPrompt = `
|
||||||
Analyse de paroles de chanson
|
<ANALYSE_PAROLES>
|
||||||
Titre: ${title || "Sans titre"}
|
<METADONNEES>
|
||||||
|
<TITRE>${title || "Sans titre"}</TITRE>
|
||||||
Paroles:
|
</METADONNEES>
|
||||||
"""
|
<PAROLES><![CDATA[
|
||||||
${lyricsText}
|
${lyricsText}
|
||||||
"""
|
]]></PAROLES>
|
||||||
|
</ANALYSE_PAROLES>
|
||||||
|
|
||||||
Consigne:
|
<DIRECTIVES>
|
||||||
- Applique la RUBRIQUE DE DÉCISION ci-dessous pour autoriser la création artistique tout en bloquant les cas réellement dangereux.
|
<OBJECTIF>Autoriser l'expression artistique tout en bloquant les contenus réellement dangereux.</OBJECTIF>
|
||||||
- "blocked" = true UNIQUEMENT pour les cas A.1 à A.7 (non ambigus).
|
<REGLE>"blocked" = true UNIQUEMENT pour les cas graves listés dans la section A.</REGLE>
|
||||||
- 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é).
|
<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>
|
||||||
- Donne des "excerpts" courts (citations EXACTES) si possible.
|
<REGLE>Fournis des "excerpts" courts en citant exactement les passages sensibles.</REGLE>
|
||||||
|
</DIRECTIVES>
|
||||||
|
|
||||||
${policy}
|
${decisionPolicy}
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
const { output } = await genkit({
|
const { output } = await genkit({
|
||||||
|
|||||||
@@ -44,25 +44,30 @@ exports.generatePicturePrompt = (project = {}) => {
|
|||||||
? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)"
|
? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)"
|
||||||
: "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)";
|
: "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 = `
|
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:
|
<CONTEXTES_VISUELS>
|
||||||
- Titre: ${title || "Sans titre"}
|
<FORMAT>Image carrée 1024x1024 pixels.</FORMAT>
|
||||||
- Style musical: ${tags.join(" ; ")}
|
<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:
|
<CONTRAINTES>
|
||||||
- Style: Abstrait, graphique, avec formes géométriques et dégradés de couleurs
|
<INTERDIT>Texte ou typographie.</INTERDIT>
|
||||||
- Couleurs: ${paletteHint}
|
<INTERDIT>Personnes ou visages.</INTERDIT>
|
||||||
- IMPORTANT: Image plein cadre sans AUCUN bord blanc ou espace vide
|
<INTERDIT>Fond blanc ou bordures.</INTERDIT>
|
||||||
- Composition: Éléments visuels répartis sur TOUTE la surface de l'image
|
<INTERDIT>Filigrane ou logo.</INTERDIT>
|
||||||
|
</CONTRAINTES>
|
||||||
Contraintes:
|
|
||||||
- PAS de texte ou typographie
|
|
||||||
- PAS de personnes ou visages
|
|
||||||
- PAS de fond blanc ou bordures
|
|
||||||
- PAS de filigrane
|
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
return prompt;
|
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 { z } = require("genkit");
|
||||||
const { generateAI, analyseLyrics } = require("../helpers/gemini");
|
const { generateAI, analyseLyrics } = require("../helpers/gemini");
|
||||||
const {
|
const {
|
||||||
@@ -10,6 +10,64 @@ const axios = require("axios");
|
|||||||
const admin = require("firebase-admin");
|
const admin = require("firebase-admin");
|
||||||
const { refList } = require("../index");
|
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 = {} }) => {
|
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -22,6 +80,52 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
|||||||
rhymes = "",
|
rhymes = "",
|
||||||
} = data;
|
} = 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);
|
console.log("Function generate lyrics start with data", data);
|
||||||
|
|
||||||
const system = `
|
const system = `
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.5 MiB |
@@ -211,6 +211,7 @@ export const tutorial = {
|
|||||||
|
|
||||||
export const background = {
|
export const background = {
|
||||||
writingBG,
|
writingBG,
|
||||||
|
writingBgWeb: require("./UI/writingBgWeb.png"),
|
||||||
studioBG,
|
studioBG,
|
||||||
studioBG2,
|
studioBG2,
|
||||||
productionBG,
|
productionBG,
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ const BorderGradientButton = ({
|
|||||||
containerStyle = {},
|
containerStyle = {},
|
||||||
tint = "dark",
|
tint = "dark",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
maxWidth = null,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ ...containerStyle, opacity: disabled ? 0.6 : 1 }}
|
style={{
|
||||||
|
maxWidth: maxWidth ? maxWidth : null,
|
||||||
|
...containerStyle,
|
||||||
|
opacity: disabled ? 0.6 : 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<BorderGradient
|
<BorderGradient
|
||||||
gradientProps={{
|
gradientProps={{
|
||||||
|
|||||||
@@ -280,6 +280,10 @@ const ListSelection = ({
|
|||||||
const itemKey = buildItemKey(String(val ?? ""));
|
const itemKey = buildItemKey(String(val ?? ""));
|
||||||
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
|
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
|
||||||
const showHoverOutline = isHovered && !sel;
|
const showHoverOutline = isHovered && !sel;
|
||||||
|
const showSelectionOutline =
|
||||||
|
!isWeb &&
|
||||||
|
(variant === "simple" || variant === "emotion" || variant === "titleDescription") &&
|
||||||
|
sel;
|
||||||
|
|
||||||
const baseContent =
|
const baseContent =
|
||||||
variant === "simple"
|
variant === "simple"
|
||||||
@@ -294,6 +298,8 @@ const ListSelection = ({
|
|||||||
};
|
};
|
||||||
if (sel && isWeb) headerContainerStyle.backgroundColor = "transparent";
|
if (sel && isWeb) headerContainerStyle.backgroundColor = "transparent";
|
||||||
|
|
||||||
|
const shouldShowBorder = !sel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[{ position: "relative" }, itemOuterStyle]}
|
style={[{ position: "relative" }, itemOuterStyle]}
|
||||||
@@ -305,13 +311,13 @@ const ListSelection = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
colors={borderColors} // mobile: rendu d’avant conservé
|
colors={borderColors}
|
||||||
tint={tint}
|
tint={tint}
|
||||||
onPress={() => toggleSelect(item)}
|
onPress={() => toggleSelect(item)}
|
||||||
gradientProps={
|
gradientProps={
|
||||||
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
|
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
|
||||||
}
|
}
|
||||||
showBorder={!sel} // mobile: bordure d’origine
|
showBorder={shouldShowBorder}
|
||||||
disableBlur={sel && isWeb} // web: pas de blur si gradient
|
disableBlur={sel && isWeb} // web: pas de blur si gradient
|
||||||
blurViewStyle={
|
blurViewStyle={
|
||||||
sel && isWeb
|
sel && isWeb
|
||||||
@@ -323,7 +329,7 @@ const ListSelection = ({
|
|||||||
{withWebBlueGradientIfSelected(baseContent, sel)}
|
{withWebBlueGradientIfSelected(baseContent, sel)}
|
||||||
</CreateLyricsHeader>
|
</CreateLyricsHeader>
|
||||||
|
|
||||||
{showHoverOutline && (
|
{(showHoverOutline || showSelectionOutline) && (
|
||||||
<View
|
<View
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
initializeAuth,
|
initializeAuth,
|
||||||
} from "firebase/auth/react-native";
|
} from "firebase/auth/react-native";
|
||||||
import firebase from "firebase/compat/app";
|
import firebase from "firebase/compat/app";
|
||||||
|
|
||||||
import "firebase/compat/auth";
|
import "firebase/compat/auth";
|
||||||
import "firebase/compat/firestore";
|
import "firebase/compat/firestore";
|
||||||
import "firebase/compat/functions";
|
import "firebase/compat/functions";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { background } from "../../assets";
|
|||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
|
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
|
||||||
import useLayoutType from "../../hooks/useLayoutType";
|
import useLayoutType, { isWeb } from "../../hooks/useLayoutType";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
@@ -336,7 +336,7 @@ const CreateLyricsWithAi = () => {
|
|||||||
headerType="NONE"
|
headerType="NONE"
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
|
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
|
||||||
backgroundImg={background.homeBG}
|
backgroundImg={background.writingBG}
|
||||||
>
|
>
|
||||||
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
|
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ const CreateLyricsWithAi = () => {
|
|||||||
headerType="NONE"
|
headerType="NONE"
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
|
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
|
||||||
backgroundImg={background.homeBG}
|
backgroundImg={background.writingBgWeb}
|
||||||
>
|
>
|
||||||
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
|
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { View, Text, Pressable, Image, Platform } from "react-native";
|
import { View, Text, Image, Platform, Alert } from "react-native";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Palette, Style } from "../../styles";
|
import { Palette, Style } from "../../styles";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { ai, icons } from "../../assets";
|
import { ai } from "../../assets";
|
||||||
import { size } from "../../styles/Style";
|
|
||||||
import ProgressBar from "../../components/ProgressBar";
|
import ProgressBar from "../../components/ProgressBar";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { navigate } from "../../navigation/NavigationService";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import firebase from "../../config/firebase";
|
import firebase from "../../config/firebase";
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
@@ -18,6 +17,8 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
const [called, setCalled] = useState(false);
|
const [called, setCalled] = useState(false);
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const hasShownErrorAlert = useRef(false);
|
||||||
const { setIsLoading } = useMinuit();
|
const { setIsLoading } = useMinuit();
|
||||||
const { updateProjectData } = useUser();
|
const { updateProjectData } = useUser();
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setSaved(false);
|
setSaved(false);
|
||||||
|
setError(null);
|
||||||
|
hasShownErrorAlert.current = false;
|
||||||
}
|
}
|
||||||
}, [active]);
|
}, [active]);
|
||||||
|
|
||||||
@@ -52,6 +55,9 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
try {
|
try {
|
||||||
setCalled(true);
|
setCalled(true);
|
||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
|
console.log("🚀 [CreatingLyrics] Lancement de la génération", {
|
||||||
|
platform: Platform.OS,
|
||||||
|
});
|
||||||
const callable = firebase
|
const callable = firebase
|
||||||
.functions()
|
.functions()
|
||||||
.httpsCallable("lyrics-generateLyrics");
|
.httpsCallable("lyrics-generateLyrics");
|
||||||
@@ -66,8 +72,22 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
});
|
});
|
||||||
setResult(data);
|
setResult(data);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
|
console.log("✨ [CreatingLyrics] Génération réussie");
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
await setIsLoading(false);
|
await setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -85,6 +105,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
if (saved) return;
|
if (saved) return;
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
|
console.log("💾 [CreatingLyrics] Sauvegarde automatique des paroles");
|
||||||
await updateProjectData({
|
await updateProjectData({
|
||||||
title: result?.title || "",
|
title: result?.title || "",
|
||||||
lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
|
lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
|
||||||
@@ -95,13 +116,13 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
});
|
});
|
||||||
navigate(Routes.Lyrics);
|
navigate(Routes.Lyrics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Auto save generated lyrics error", e?.message);
|
console.error("⚠️ [CreatingLyrics] Erreur lors de la sauvegarde", e);
|
||||||
} finally {
|
} finally {
|
||||||
await setIsLoading(false);
|
await setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (active && result && progress >= 100 && !saved) {
|
if (active && result && progress >= 100 && !saved && !error) {
|
||||||
autoSaveAndGo();
|
autoSaveAndGo();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -109,12 +130,39 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
result,
|
result,
|
||||||
progress,
|
progress,
|
||||||
saved,
|
saved,
|
||||||
|
error,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
updateProjectData,
|
updateProjectData,
|
||||||
config,
|
config,
|
||||||
selections,
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -169,7 +217,9 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
textAlign: "center",
|
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"}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ alignItems: "center", gap: 16 }}>
|
<View style={{ alignItems: "center", gap: 16 }}>
|
||||||
<ProgressBar gradient progress={progress} />
|
<ProgressBar gradient progress={progress} />
|
||||||
@@ -183,7 +233,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
{progress}%
|
{progress}%
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{!saved && (
|
{!saved && result && !error && (
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Découvrir mon texte"
|
title="Découvrir mon texte"
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
@@ -195,6 +245,9 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
if (saved) return;
|
if (saved) return;
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
|
console.log(
|
||||||
|
"📄 [CreatingLyrics] Consultation manuelle du texte",
|
||||||
|
);
|
||||||
await updateProjectData({
|
await updateProjectData({
|
||||||
title: result?.title || "",
|
title: result?.title || "",
|
||||||
lyrics: Array.isArray(result?.lyrics)
|
lyrics: Array.isArray(result?.lyrics)
|
||||||
@@ -206,7 +259,10 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
});
|
});
|
||||||
navigate(Routes.Lyrics);
|
navigate(Routes.Lyrics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Save generated lyrics error", e?.message);
|
console.error(
|
||||||
|
"⚠️ [CreatingLyrics] Erreur lors de l'ouverture manuelle",
|
||||||
|
e,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await setIsLoading(false);
|
await setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { navigate } from "../../navigation/NavigationService";
|
|||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { gutters } from "../../styles";
|
import { gutters } from "../../styles";
|
||||||
import CustomInput from "./components/CustomInput";
|
import CustomInput from "./components/CustomInput";
|
||||||
|
import { background } from "../../assets";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
|
|
||||||
const Lyrics = ({ navigation }) => {
|
const Lyrics = ({ navigation }) => {
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
@@ -239,7 +241,10 @@ const Lyrics = ({ navigation }) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page headerType="NONE">
|
<Page
|
||||||
|
backgroundImg={isWeb ? background.writingBgWeb : background.writingBG}
|
||||||
|
headerType="NONE"
|
||||||
|
>
|
||||||
<MusicLandHeader
|
<MusicLandHeader
|
||||||
onPressBack={() => navigate(Routes.Home)}
|
onPressBack={() => navigate(Routes.Home)}
|
||||||
progress={95}
|
progress={95}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
|||||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import Page from "../../layouts/Page";
|
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import { useUserData } from "../../providers/UserDataProvider";
|
import { useUserData } from "../../providers/UserDataProvider";
|
||||||
import { gutters } from "../../styles";
|
import { gutters } from "../../styles";
|
||||||
|
import { background } from "../../assets";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
|
|
||||||
const WritingLyrics = () => {
|
const WritingLyrics = () => {
|
||||||
const { createNewProject, selectedProject, updateProjectData } =
|
const { createNewProject, selectedProject, updateProjectData } =
|
||||||
@@ -42,7 +44,10 @@ const WritingLyrics = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page headerType="NONE">
|
<Page
|
||||||
|
backgroundImg={isWeb ? background.writingBgWeb : background.writingBG}
|
||||||
|
headerType="NONE"
|
||||||
|
>
|
||||||
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
|
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
|
||||||
<MusicLandHeader onPressBack={goBack} progress={9} />
|
<MusicLandHeader onPressBack={goBack} progress={9} />
|
||||||
<View
|
<View
|
||||||
@@ -56,9 +61,13 @@ const WritingLyrics = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BorderGradientButton
|
<BorderGradientButton
|
||||||
|
maxWidth={500}
|
||||||
onPress={() => createAndNavigate({ hasLyrics: true })}
|
onPress={() => createAndNavigate({ hasLyrics: true })}
|
||||||
|
containerStyle={{ alignSelf: "center", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
|
maxWidth={500}
|
||||||
|
containerStyle={{ alignSelf: "center", width: "100%" }}
|
||||||
title="Écrire des paroles avec une IA"
|
title="Écrire des paroles avec une IA"
|
||||||
onPress={() => createAndNavigate({ hasLyrics: false })}
|
onPress={() => createAndNavigate({ hasLyrics: false })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user