filter on lyrics for bloking unwanted content, tickets fix

This commit is contained in:
Thomas Demirdjian
2025-09-08 15:48:29 +02:00
parent 987388c501
commit a3dc4f89b7
22 changed files with 1386 additions and 126 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { useRoute } from "@react-navigation/core";
import { useRoute } from "@react-navigation/native";
import React, { useState } from "react";
import { View } from "react-native";
import { background } from "../../assets";
+1 -1
View File
@@ -10,7 +10,7 @@ import Style, { size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/core";
import { useRoute } from "@react-navigation/native";
const DownloadPrices = () => {
const params = useRoute().params;
+1 -1
View File
@@ -12,7 +12,7 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/core";
import { useRoute } from "@react-navigation/native";
const SongDownloaded = () => {
const params = useRoute().params;
+1 -1
View File
@@ -12,7 +12,7 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/core";
import { useRoute } from "@react-navigation/native";
const SongRelease = () => {
const params = useRoute().params;
+1 -1
View File
@@ -10,7 +10,7 @@ import Style, { size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/core";
import { useRoute } from "@react-navigation/native";
const STREAM_PRICES = [
{
+4 -5
View File
@@ -31,13 +31,12 @@ const ChooseRhythm = ({ selected, setSelected }) => {
<CreateLyricsHeader
onPress={() => onPressSelect(item)}
tint={selectedItem ? "default" : "dark"}
colors={
selectedItem
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
// Keep transparent colors; highlight selection with border only
colors={[Palette.tran, Palette.tran]}
containerStyle={{
...styles.itemContainer,
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
}}
>
<View
+13 -5
View File
@@ -11,8 +11,8 @@ import ChooseGenre from "./ChooseGenre";
import CustomizeVoice from "./CustomizeVoice";
import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm";
import { projectsRef } from "../../config/firebase";
import { useUser } from "../../providers/UserDataProvider";
import firebase from "../../config/firebase";
import { Routes } from "../../navigation";
const { width } = Dimensions.get("window");
@@ -26,7 +26,8 @@ const ComposeSong = () => {
// Selections state
const [genres, setGenres] = useState([]);
const [voice, setVoice] = useState(null);
// voice: object keyed by category (e.g., { base: string|null, Sensibilité: string|null, Technique: string|null })
const [voice, setVoice] = useState({});
const [instruments, setInstruments] = useState([]);
const [rhythm, setRhythm] = useState(null);
@@ -35,7 +36,8 @@ const ComposeSong = () => {
case 0:
return Array.isArray(genres) && genres.length > 0;
case 1:
return !!voice;
// Must select at least one in base category
return !!(voice && typeof voice === "object" && voice.base);
case 2:
return Array.isArray(instruments) && instruments.length > 0;
case 3:
@@ -58,11 +60,15 @@ const ComposeSong = () => {
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
}
const voiceArray = Object.entries(voice || {})
.filter(([, v]) => typeof v === "string" && v.trim())
.map(([category, value]) => ({ category, value }));
return {
title: selectedProject?.title || "",
lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [],
voice: voice || undefined,
voice: voiceArray,
instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined,
projectId: selectedProjectId || undefined,
@@ -83,13 +89,15 @@ const ComposeSong = () => {
genres: Array.isArray(musicConfig?.genres)
? musicConfig.genres
: [],
voice: musicConfig?.voice || "",
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
instruments: Array.isArray(musicConfig?.instruments)
? musicConfig.instruments
: [],
tempo: musicConfig?.tempo || "",
},
musicStatus: null,
sunoTaskId: firebase.firestore.FieldValue.delete(),
musicUrls: firebase.firestore.FieldValue.delete(),
});
}
} catch (e) {}
+20 -13
View File
@@ -7,13 +7,18 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import { BlurView } from "expo-blur";
import { VOICE } from "../../data/data";
const CustomizeVoice = ({ selected, setSelected }) => {
const CustomizeVoice = ({ selected = {}, setSelected }) => {
const [containerLayout, setContainerLayout] = useState(null);
const onPressSelect = (item) => {
// Toggle select: only 1 per category (section.title)
const onPressSelect = (category, item) => {
if (!setSelected) return;
if (selected === item) setSelected(null);
else setSelected(item);
const current = selected && typeof selected === "object" ? selected : {};
// If tapping the same item, deselect; otherwise, set new one for the category
const next = { ...current };
if (current[category] === item) next[category] = null;
else next[category] = item;
setSelected(next);
};
return (
@@ -32,25 +37,26 @@ const CustomizeVoice = ({ selected, setSelected }) => {
sections={VOICE}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.contentContainer}
renderItem={({ item }) => {
const selectedItem = selected === item;
renderItem={({ item, section }) => {
const cat = section?.title;
const selectedItem = selected?.[cat] === item;
return (
<CreateLyricsHeader
onPress={() => onPressSelect(item)}
onPress={() => onPressSelect(cat, item)}
tint={selectedItem ? "default" : "dark"}
colors={
selectedItem
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
// Ne pas changer le blur; garder fond transparent comme ChooseInstruments
colors={[Palette.tran, Palette.tran]}
containerStyle={{
...styles.itemContainer,
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
}}
>
<View
style={{
paddingVertical: 6,
height: 40,
justifyContent: "center",
}}
>
<Text style={styles.itemText}>{item}</Text>
@@ -89,6 +95,7 @@ const styles = StyleSheet.create({
paddingBottom: 50,
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
borderRadius: 14,
overflow: "hidden",
+59 -23
View File
@@ -25,8 +25,9 @@ const GeneratingSong = () => {
const progressTimerRef = useRef(null);
const navigatedRef = useRef(false);
const isFocused = useIsFocused();
const lastConfigKeyRef = useRef(null);
const askedRef = useRef(false);
const callingRef = useRef(false);
const localStartRef = useRef(null);
// project loaded from provider
@@ -39,26 +40,49 @@ const GeneratingSong = () => {
progressTimerRef.current = null;
}
};
if (selectedProject?.musicStatus === "GENERATED") {
const status = selectedProject?.musicStatus;
if (status !== "GENERATING") {
// Reset local start when leaving generating state
localStartRef.current = null;
}
if (status === "GENERATED") {
setProgress(100);
clearTimer();
return () => clearTimer();
}
const resolveStartDate = () => {
const gs = selectedProject?.generationStartAt;
if (gs?.toDate) return gs.toDate();
if (gs) return new Date(gs);
if (status === "GENERATING") {
if (!localStartRef.current) localStartRef.current = new Date();
return localStartRef.current;
}
return null;
};
const update = () => {
const startDate = selectedProject?.generationStartAt?.toDate
? selectedProject.generationStartAt.toDate()
: new Date(selectedProject?.generationStartAt || Date.now());
const startDate = resolveStartDate();
if (!startDate) {
setProgress(0);
return;
}
const elapsed = moment().diff(moment(startDate));
const raw = Math.floor((elapsed / totalMs) * 100);
// While status is GENERATING, block visual progress at 99%
const pct = Math.max(0, Math.min(99, raw));
setProgress(pct);
};
update();
clearTimer();
progressTimerRef.current = setInterval(update, 1000);
return () => clearTimer();
}, [selectedProject?.musicStatus]);
}, [selectedProject?.musicStatus, selectedProject?.generationStartAt]);
// Auto navigate to SongReady when generation completed
useEffect(() => {
@@ -76,7 +100,7 @@ const GeneratingSong = () => {
title: cfg?.title || "",
lyrics: Array.isArray(cfg?.lyrics) ? cfg.lyrics : [],
genres: Array.isArray(cfg?.genres) ? cfg.genres : [],
voice: cfg?.voice || "",
voice: Array.isArray(cfg?.voice) ? cfg.voice : [],
instruments: Array.isArray(cfg?.instruments) ? cfg.instruments : [],
tempo: cfg?.tempo || "",
};
@@ -131,37 +155,49 @@ const GeneratingSong = () => {
}
}
// Trigger generation only when focused; ask once per config
// Ensure we never call generation more than once concurrently
const startMusicGenerationOnce = React.useCallback(() => {
if (callingRef.current) return;
callingRef.current = true;
startMusicGeneration()
.catch(() => {})
.finally(() => {
// Keep locked while status transitions to GENERATING; will be prevented by guards
setTimeout(() => {
callingRef.current = false;
}, 500);
});
}, []);
// Trigger generation only when focused; ask once while idle
useEffect(() => {
if (!isFocused) return;
const status = selectedProject?.musicStatus;
const titleOk = !!selectedProject?.title;
// Idle if not actively generating or generated (ignore stale taskId)
const isIdle =
status == null ||
status === "" ||
status === "PENDING" ||
status === "READY";
const key = JSON.stringify(effectiveConfig || {});
if (lastConfigKeyRef.current !== key) {
lastConfigKeyRef.current = key;
askedRef.current = false;
}
const canAsk =
!!selectedProject?.title && selectedProject?.musicStatus !== "GENERATING";
if (canAsk && !askedRef.current) {
if (titleOk && isIdle && !askedRef.current) {
askedRef.current = true;
Alert.alert("Attention", "Une génération va être lancée. Continuer ?", [
{
text: "Non",
style: "cancel",
onPress: () => {
askedRef.current = false;
},
},
{
text: "Oui",
onPress: () => startMusicGeneration(),
},
{ text: "Oui", onPress: () => startMusicGenerationOnce() },
]);
}
}, [
isFocused,
selectedProject?.title,
selectedProject?.musicStatus,
effectiveConfig,
]);
return (
+1 -1
View File
@@ -8,7 +8,7 @@ import { goBack } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import GradientButton from "../../components/GradientButton";
import { useRoute } from "@react-navigation/core";
import { useRoute } from "@react-navigation/native";
const Regenerate = () => {
const params = useRoute().params;
+6 -1
View File
@@ -51,8 +51,13 @@ const EmotionConvey = ({
<View style={{ paddingHorizontal: 5 }}>
<CreateLyricsHeader
colors={item.color}
tint={selectedItem ? "default" : "dark"}
tint={"dark"}
onPress={() => onPressSelect(item)}
containerStyle={{
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
borderRadius: 14,
}}
>
<Text
style={{
+5 -6
View File
@@ -37,18 +37,16 @@ const Goals = ({ selected: selectedProp, setSelected: setSelectedProp, otherObje
<CreateLyricsHeader
onPress={() => onPressSelect(item)}
tint={selectedItem ? "default" : "dark"}
colors={
selectedItem
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
colors={[Palette.tran, Palette.tran]}
containerStyle={{
...styles.itemContainer,
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
}}
>
<View
style={{
height: 40,
height: 54,
...Style.containerCenter,
}}
>
@@ -81,6 +79,7 @@ const styles = StyleSheet.create({
paddingBottom: 50,
},
itemContainer: {
height: 54,
borderRadius: 14,
shadowColor: "#00000040",
shadowOffset: {
+74 -32
View File
@@ -1,5 +1,5 @@
import { View, ScrollView, Alert } from "react-native";
import React, { useMemo, useState, useCallback, useEffect, useRef } from "react";
import React, { useMemo, useState, useCallback, useEffect } from "react";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import { navigate } from "../../navigation/NavigationService";
@@ -61,37 +61,6 @@ const Lyrics = ({ navigation }) => {
}, [projectTitle, projectLyrics, projectConfig]);
const [titleValue, setTitleValue] = useState(initial.title || "");
const titleSaveTimer = useRef(null);
// Auto-save du titre lorsqu'il est modifié (si non vide)
useEffect(() => {
const newTitle = (titleValue || "").trim();
// Annuler tout timer précédent
if (titleSaveTimer.current) clearTimeout(titleSaveTimer.current);
// Ne rien faire si inchangé vs projet courant
const currentTitle = (selectedProject?.title || "").trim();
if (!newTitle || newTitle === currentTitle) return;
titleSaveTimer.current = setTimeout(async () => {
try {
await projectsRef.doc(selectedProjectId).set(
{
title: newTitle,
titleLower: newTitle.toLowerCase(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
} catch (e) {
// silencieux: l'utilisateur pourra tjs valider plus tard
console.log("Auto-save titre échoué", e?.message);
}
}, 500);
return () => {
if (titleSaveTimer.current) clearTimeout(titleSaveTimer.current);
};
}, [titleValue, selectedProjectId, selectedProject]);
const [sections, setSections] = useState(initial.sections || []);
const setSectionAt = (index, value) => {
setSections((prev) => {
@@ -158,6 +127,79 @@ const Lyrics = ({ navigation }) => {
s.lyrics === normalizedNewLyrics[i]?.lyrics,
);
// 1) Appel de la Cloud Function de modération avant tout enregistrement
try {
const callable = firebase
.functions()
.httpsCallable("lyrics-analyseLyricsToxicity");
const { data } = await callable({
title: titleTrimmed,
lyrics: normalizedNewLyrics,
});
if (!data?.success) {
// Blocage dur: afficher le message et ne pas sauvegarder
if (data?.errorCode === "TOXIC_CONTENT_BLOCKED") {
const quotes = Array.isArray(data?.result?.excerpts)
? data.result.excerpts
.slice(0, 3)
.map((e) => `${e.quote}`)
.join("\n")
: null;
Alert.alert(
"Contenu interdit",
[data?.message, quotes].filter(Boolean).join("\n\n"),
);
return; // stop here
}
// Erreur d'analyse: informer et arrêter
if (data?.errorCode === "ANALYSE_FAILED") {
Alert.alert(
"Analyse indisponible",
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.",
);
return;
}
}
// Cas signalé mais non bloquant: demander confirmation
if (data?.errorCode === "TOXIC_CONTENT_FLAGGED") {
const quotes = Array.isArray(data?.result?.excerpts)
? data.result.excerpts
.slice(0, 3)
.map((e) => `${e.quote}`)
.join("\n")
: null;
const msg = [data?.message, quotes].filter(Boolean).join("\n\n");
const proceed = await new Promise((resolve) => {
Alert.alert("Contenu potentiellement sensible", msg, [
{
text: "Annuler",
style: "cancel",
onPress: () => resolve(false),
},
{
text: "Continuer",
style: "destructive",
onPress: () => resolve(true),
},
]);
});
if (!proceed) return;
}
} catch (moderationError) {
console.log(
"Moderation call failed",
moderationError?.message || moderationError,
);
Alert.alert(
"Analyse indisponible",
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.",
);
return;
}
const baseData = {
title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(),
+4 -6
View File
@@ -33,18 +33,16 @@ const Rhymes = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
key={index}
onPress={() => onPressSelect(item)}
tint={selectedItem ? "default" : "dark"}
colors={
selectedItem
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
colors={[Palette.tran, Palette.tran]}
containerStyle={{
...styles.itemContainer,
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
}}
>
<View
style={{
height: 40,
height: 54,
...Style.containerCenter,
paddingHorizontal: 14,
}}
+3 -5
View File
@@ -40,13 +40,11 @@ const SongStructure = ({
<CreateLyricsHeader
onPress={() => onPressSelect(item)}
tint={selectedItem ? "default" : "dark"}
colors={
selectedItem
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
colors={[Palette.tran, Palette.tran]}
containerStyle={{
...styles.itemContainer,
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
}}
>
<View
+3 -5
View File
@@ -43,15 +43,13 @@ const SongStyle = ({
return (
<CreateLyricsHeader
colors={
selectedItem
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
colors={[Palette.tran, Palette.tran]}
tint={selectedItem ? "default" : "dark"}
onPress={() => onPressSelect(item.title)}
containerStyle={{
...styles.itemContainer,
borderWidth: selectedItem ? 2 : 0,
borderColor: selectedItem ? "#F94697" : "transparent",
}}
>
<View style={{ paddingVertical: 6 }}>