continue fix generating flow

This commit is contained in:
Thomas Demirdjian
2025-09-02 15:42:45 +02:00
parent d634c55aaa
commit 07053bc8db
10 changed files with 314 additions and 95 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

+3 -12
View File
@@ -39,17 +39,6 @@ const ValidateModal = ({ visible, onClose, onPressValidate }) => {
> >
Attention ! Attention !
</Text> </Text>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Lorsque tu clique sur valider, tu ne pourra plus changer ni le
texte ni la mélodie.{" "}
</Text>
</View>
<Text <Text
style={{ style={{
fontSize: 16, fontSize: 16,
@@ -58,9 +47,11 @@ const ValidateModal = ({ visible, onClose, onPressValidate }) => {
textAlign: "center", textAlign: "center",
}} }}
> >
Valider Lorsque tu clique sur valider, tu ne pourra plus changer ni le
texte ni la mélodie.
</Text> </Text>
</View> </View>
</View>
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}> <View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
<BorderGradientButton title="Retour" onPress={onClose} /> <BorderGradientButton title="Retour" onPress={onClose} />
<GradientButton <GradientButton
+5
View File
@@ -45,6 +45,7 @@ import RecordedPlayback from "../screens/Playback/RecordedPlayback";
import ChooseDecor from "../screens/Playback/ChooseDecor"; import ChooseDecor from "../screens/Playback/ChooseDecor";
import CreatingDecor from "../screens/Playback/CreatingDecor"; import CreatingDecor from "../screens/Playback/CreatingDecor";
import VideoFinalize from "../screens/Playback/VideoFinalize"; import VideoFinalize from "../screens/Playback/VideoFinalize";
import GeneratingSong from "../screens/Studio/GeneratingSong";
import AllMyMusic from "../screens/Library/AllMyMusic"; import AllMyMusic from "../screens/Library/AllMyMusic";
import AllMyPlaylist from "../screens/Library/AllMyPlaylist"; import AllMyPlaylist from "../screens/Library/AllMyPlaylist";
import AllMyLikedMusic from "../screens/Library/AllMyLikedMusic"; import AllMyLikedMusic from "../screens/Library/AllMyLikedMusic";
@@ -185,6 +186,10 @@ const screens = [
name: Routes.StreamSong, name: Routes.StreamSong,
component: StreamSong, component: StreamSong,
}, },
{
name: Routes.GeneratingSong,
component: GeneratingSong,
},
{ {
name: Routes.SongRelease, name: Routes.SongRelease,
component: SongRelease, component: SongRelease,
+1
View File
@@ -49,6 +49,7 @@ export const Routes = {
StreamSong: "StreamSong", StreamSong: "StreamSong",
SongRelease: "SongRelease", SongRelease: "SongRelease",
PlaybackExample: "PlaybackExample", PlaybackExample: "PlaybackExample",
GeneratingSong: "GeneratingSong",
PlaybackOnboarding: "PlaybackOnboarding", PlaybackOnboarding: "PlaybackOnboarding",
Playback: "Playback", Playback: "Playback",
+9 -4
View File
@@ -174,17 +174,22 @@ const MusicDetails = () => {
} }
}; };
const description = useMemo(() => { const description = useMemo(() => {
// Try to build a readable text from lyrics if present // Build a readable text from lyrics with section labels
if (Array.isArray(project?.lyrics)) { if (Array.isArray(project?.lyrics)) {
return project.lyrics return project.lyrics
.map((s) => (s?.lyrics || "").trim()) .map((s) => {
const body = (s?.lyrics || "").trim();
if (!body) return null;
const t = (s?.type || "").toLowerCase();
const label = t === "refrain" ? "Refrain" : "Couplet";
return `[${label}]\n${body}`;
})
.filter(Boolean) .filter(Boolean)
.slice(0, 4)
.join("\n\n"); .join("\n\n");
} }
const c = project?.lyrics?.couplet; const c = project?.lyrics?.couplet;
const r = project?.lyrics?.refrain; const r = project?.lyrics?.refrain;
const parts = [c, r].filter(Boolean); const parts = [c ? `[Couplet]\n${c}` : null, r ? `[Refrain]\n${r}` : null].filter(Boolean);
return parts.length ? parts.join("\n\n") : ""; return parts.length ? parts.join("\n\n") : "";
}, [project]); }, [project]);
+9 -2
View File
@@ -10,6 +10,7 @@ import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider"; import { useUserData } from "../providers/UserDataProvider";
import FontAwesome from "@expo/vector-icons/FontAwesome"; import FontAwesome from "@expo/vector-icons/FontAwesome";
import palette from "../styles/Palette"; import palette from "../styles/Palette";
import GeneratingSong from "./Studio/GeneratingSong";
const CREATE_DATA = [ const CREATE_DATA = [
{ {
@@ -77,13 +78,19 @@ const NewMusicOptions = ({ route }) => {
}); });
break; break;
case 1: case 1:
if (currentProjet?.musicStatus === "GENERATING") {
navigate(Routes.GeneratingSong, { projectId: currentProjet.id });
} else if (currentProjet?.musicStatus === "GENERATED") {
navigate(Routes.SongReady, { projectId: currentProjet.id });
} else {
navigate(Routes.Compose, { projectId: currentProjet.id }); navigate(Routes.Compose, { projectId: currentProjet.id });
}
break; break;
case 2: case 2:
navigate(Routes.PouchReady, { projectId: currentProjet.id }); navigate(Routes.PouchReady, { projectId: currentProjet.id });
break; break;
default: case 3:
navigate(Routes.PouchReady, { projectId: currentProjet.id }); navigate(Routes.Playback, { projectId: currentProjet.id });
break; break;
} }
}; };
+12 -23
View File
@@ -4,17 +4,17 @@ import Page from "../../layouts/Page";
import { background } from "../../assets"; import { background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { goBack } from "../../navigation/NavigationService"; import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles"; import { gutters } from "../../styles";
import SwiperFlatList from "react-native-swiper-flatlist"; import SwiperFlatList from "react-native-swiper-flatlist";
import ChooseGenre from "./ChooseGenre"; import ChooseGenre from "./ChooseGenre";
import CustomizeVoice from "./CustomizeVoice"; import CustomizeVoice from "./CustomizeVoice";
import ChooseInstruments from "./ChooseInstruments"; import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm"; import ChooseRhythm from "./ChooseRhythm";
import CreatingSong from "./CreatingSong";
import { useRoute } from "@react-navigation/native"; import { useRoute } from "@react-navigation/native";
import firebase from "../../config/firebase"; import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef"; import useDataFromRef from "../../hooks/useDataFromRef";
import { Routes } from "../../navigation";
const { width } = Dimensions.get("window"); const { width } = Dimensions.get("window");
@@ -27,20 +27,14 @@ const ComposeSong = () => {
const projectId = route?.params?.projectId; const projectId = route?.params?.projectId;
// Selections state // Selections state
const [genres, setGenres] = useState(__DEV__ ? ["Hip Hop/Rap", "Punk"] : []); const [genres, setGenres] = useState([]);
const [voice, setVoice] = useState( const [voice, setVoice] = useState(null);
__DEV__ ? "Deux voix pour interpreter ta chanson" : null, const [instruments, setInstruments] = useState([]);
); const [rhythm, setRhythm] = useState(null);
const [instruments, setInstruments] = useState(
__DEV__ ? ["Synthétiseur", "Trompette", "Piano classique"] : [],
);
const [rhythm, setRhythm] = useState(__DEV__ ? "Rapide" : null);
// Fetch selected project to get title + lyrics // Fetch selected project to get title + lyrics
const { data: project } = useDataFromRef({ const { data: project } = useDataFromRef({
ref: projectId ref: projectId ? projectsRef.doc(projectId) : null,
? firebase.firestore().collection("projects").doc(projectId)
: null,
simpleRef: true, simpleRef: true,
listener: true, listener: true,
condition: !!projectId, condition: !!projectId,
@@ -86,6 +80,10 @@ const ComposeSong = () => {
}, [project, genres, voice, instruments, rhythm, projectId]); }, [project, genres, voice, instruments, rhythm, projectId]);
const onPressNext = () => { const onPressNext = () => {
if (selectedIndex === 3) {
navigate(Routes.GeneratingSong, { config: musicConfig, projectId });
return;
}
setSelectedIndex(selectedIndex + 1); setSelectedIndex(selectedIndex + 1);
setProgress(progress + 9); setProgress(progress + 9);
scrollRef.current.scrollToIndex({ scrollRef.current.scrollToIndex({
@@ -159,15 +157,6 @@ const ComposeSong = () => {
> >
<ChooseRhythm selected={rhythm} setSelected={setRhythm} /> <ChooseRhythm selected={rhythm} setSelected={setRhythm} />
</View> </View>
<View
style={{
width: width,
height: containerLayout?.height,
paddingHorizontal: gutters,
}}
>
<CreatingSong active={selectedIndex === 4} config={musicConfig} />
</View>
</SwiperFlatList> </SwiperFlatList>
</View> </View>
{selectedIndex !== 4 && ( {selectedIndex !== 4 && (
+3 -6
View File
@@ -9,14 +9,14 @@ import ProgressBar from "../../components/ProgressBar";
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import firebase from "../../config/firebase"; import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import moment from "moment"; import moment from "moment";
const CreatingSong = ({ active, config }) => { const CreatingSong = ({ active, config }) => {
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [called, setCalled] = useState(false); const [called, setCalled] = useState(false);
const [result, setResult] = useState(null); const [, setResult] = useState(null);
const { setIsLoading } = useMinuit(); const { setIsLoading } = useMinuit();
const [musicStatus, setMusicStatus] = useState(null); const [musicStatus, setMusicStatus] = useState(null);
const [generationStartAt, setGenerationStartAt] = useState(null); const [generationStartAt, setGenerationStartAt] = useState(null);
@@ -110,10 +110,7 @@ const CreatingSong = ({ active, config }) => {
// Si un projectId et un taskId existent, mettre à jour le projet // Si un projectId et un taskId existent, mettre à jour le projet
if (config?.projectId && taskId) { if (config?.projectId && taskId) {
const projectRef = firebase const projectRef = projectsRef.doc(config.projectId);
.firestore()
.collection("projects")
.doc(config.projectId);
await projectRef.set( await projectRef.set(
{ {
+228
View File
@@ -0,0 +1,228 @@
import React, { useEffect, useRef, useState } from "react";
import { Image, Platform, Text, View } from "react-native";
import Page from "../../layouts/Page";
import { ai, background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { BlurView } from "expo-blur";
import { Palette } from "../../styles";
import ProgressBar from "../../components/ProgressBar";
import GradientButton from "../../components/GradientButton";
import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import moment from "moment";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { FONT_FAMILY } from "../../styles/Fonts";
import useDataFromRef from "../../hooks/useDataFromRef";
const GeneratingSong = ({ route }) => {
const { config, projectId } = route.params;
const [progress, setProgress] = useState(0);
const { setIsLoading } = useMinuit();
const progressTimerRef = useRef(null);
const navigatedRef = useRef(false);
const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null,
simpleRef: true,
listener: true,
condition: !!projectId,
refreshArray: [projectId],
});
// Progress based on 8 minutes cap or until status changes
useEffect(() => {
const totalMs = 8 * 60 * 1000;
const clearTimer = () => {
if (progressTimerRef.current) {
clearInterval(progressTimerRef.current);
progressTimerRef.current = null;
}
};
if (project?.musicStatus !== "GENERATING") {
setProgress(100);
clearTimer();
return () => clearTimer();
}
const update = () => {
const startDate = project?.generationStartAt?.toDate
? project.generationStartAt.toDate()
: new Date(project?.generationStartAt || Date.now());
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();
}, [project?.musicStatus]);
// Auto navigate to SongReady when generation completed
useEffect(() => {
if (!config?.projectId) return;
if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) {
navigatedRef.current = true;
navigate(Routes.SongReady, { projectId: config.projectId });
}
}, [project?.musicStatus, config?.projectId]);
async function startMusicGeneration() {
try {
console.log("startMusicGeneration");
await setIsLoading(true);
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
const { data } = await callable({
title: config?.title,
lyrics: config?.lyrics,
genres: config?.genres,
voice: config?.voice,
instruments: config?.instruments,
tempo: config?.tempo,
projectId: config?.projectId,
});
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
if (config?.projectId && taskId) {
const baseUpdate = {
sunoTaskId: taskId,
musicStatus: "GENERATING",
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
};
const updatePayload = project?.musicConfig
? baseUpdate
: {
...baseUpdate,
musicConfig: {
title: config?.title || "",
lyrics: config?.lyrics || [],
genres: config?.genres || [],
voice: config?.voice || "",
instruments: config?.instruments || [],
tempo: config?.tempo || "",
},
};
await projectsRef
.doc(config.projectId)
.set(updatePayload, { merge: true });
}
} catch (e) {
console.log("GeneratingSong error", e?.message);
} finally {
await setIsLoading(false);
}
}
// Trigger generation only if not already GENERATING
useEffect(() => {
if (!!project?.title && project?.musicStatus !== "GENERATING") {
startMusicGeneration();
}
}, [project]);
return (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader onPressBack={goBack} progress={63} />
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta musique est en cours de création !"
subTitle="Encore un peu de patience"
containerStyle={{ marginBottom: responsiveHeight(2) }}
/>
<View
style={{
height: responsiveHeight(50),
marginTop: responsiveHeight(10),
gap: 16,
}}
>
<View
style={{
zIndex: 1,
position: "absolute",
top: -150,
width: "50%",
height: "100%",
alignSelf: "center",
}}
>
<Image
source={ai.theo}
style={{ width: "100%", height: "100%", right: -10 }}
resizeMode="contain"
/>
</View>
<BlurView
intensity={40}
tint="dark"
style={{
flex: 1,
borderRadius: 16,
overflow: "hidden",
padding: 12,
backgroundColor: "#FFFFFF0A",
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<View style={{ flex: 1 }}>
<BlurView
intensity={40}
tint="dark"
style={{ flex: 1, padding: 10, paddingBottom: 20 }}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
}}
>
Ta musique est{"\n"}en cours de création
</Text>
<View style={{ alignItems: "center", gap: 16 }}>
<ProgressBar gradient progress={progress} />
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{progress}%
</Text>
</View>
<GradientButton
title={"Création en cours..."}
disabled={project?.musicStatus === "GENERATING"}
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() =>
navigate(Routes.SongReady, {
projectId: config?.projectId,
})
}
/>
</View>
</BlurView>
</View>
</BlurView>
</View>
</View>
</Page>
);
};
export default GeneratingSong;
+38 -42
View File
@@ -13,14 +13,12 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import ValidateModal from "../../components/modal/ValidateModal"; import ValidateModal from "../../components/modal/ValidateModal";
import { useRoute } from "@react-navigation/core"; import firebase, { projectsRef } from "../../config/firebase";
import firebase from "../../config/firebase";
import { useAudioPlayer } from "expo-audio"; import { useAudioPlayer } from "expo-audio";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
const SongReady = () => { const SongReady = ({ route }) => {
const params = useRoute().params || {}; const { projectId = null } = route?.params || {};
const projectId = params?.projectId;
const [showValidateModal, setShowValidateModal] = useState(false); const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]); const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
@@ -35,6 +33,7 @@ const SongReady = () => {
const player1 = useAudioPlayer( const player1 = useAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined, musicUrls[1] ? { uri: musicUrls[1] } : undefined,
); );
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
// Charger les URLs depuis le document projet // Charger les URLs depuis le document projet
useEffect(() => { useEffect(() => {
@@ -118,18 +117,14 @@ const SongReady = () => {
try { try {
const url = musicUrls[selectedIndex]; const url = musicUrls[selectedIndex];
if (!projectId || !url) return; if (!projectId || !url) return;
await firebase await projectsRef.doc(projectId).set(
.firestore()
.collection("projects")
.doc(projectId)
.set(
{ {
songUrl: url, songUrl: url,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
}, },
{ merge: true }, { merge: true },
); );
navigate(Routes.PouchReady, { projectId }); navigate(Routes.FlowSelection, { projectId });
} catch (e) { } catch (e) {
console.log("Validate error", e?.message); console.log("Validate error", e?.message);
} }
@@ -137,36 +132,7 @@ const SongReady = () => {
const onPressRegenerate = async () => { const onPressRegenerate = async () => {
try { try {
if (!projectId) return goBack(); navigate(Routes.GeneratingSong, { projectId });
const doc = await firebase
.firestore()
.collection("projects")
.doc(projectId)
.get();
const project = doc.data() || {};
const musicConfig = project?.musicConfig || {};
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
const { data } = await callable({
...musicConfig,
projectId,
});
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
if (taskId) {
await firebase.firestore().collection("projects").doc(projectId).set(
{
sunoTaskId: taskId,
musicStatus: "GENERATING",
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
}
} catch (e) { } catch (e) {
console.log("Regenerate error", e?.message); console.log("Regenerate error", e?.message);
} finally { } finally {
@@ -176,7 +142,10 @@ const SongReady = () => {
return ( return (
<Page headerType="NONE" backgroundImg={background.studioBG2}> <Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader onPressBack={goBack} progress={63} /> <MusicLandHeader
onPressBack={() => navigate(Routes.FlowSelection, { projectId })}
progress={63}
/>
<View style={{ flex: 1, marginTop: 16 }}> <View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader <CreateLyricsHeader
title="Ta chanson est prête !" title="Ta chanson est prête !"
@@ -226,7 +195,34 @@ const SongReady = () => {
: 0 : 0
} }
seekEnabled={!!musicUrls[idx]} seekEnabled={!!musicUrls[idx]}
onSeekStart={async () => {
try {
const player = idx === 0 ? player0 : player1;
wasPlayingBeforeSeek.current[idx] = !!player?.playing;
if (player?.playing) {
await player.pause?.();
setIsPlaying((p) => ({ ...p, [idx]: false }));
}
} catch (e) {
console.log(
"SongReady pause on seek start",
e?.message,
);
}
}}
onSeek={(ratio) => onSeek(idx, ratio)} onSeek={(ratio) => onSeek(idx, ratio)}
onSeekEnd={async () => {
try {
const player = idx === 0 ? player0 : player1;
if (player && wasPlayingBeforeSeek.current[idx]) {
await player.play?.();
setIsPlaying((p) => ({ ...p, [idx]: true }));
}
wasPlayingBeforeSeek.current[idx] = false;
} catch (e) {
console.log("SongReady resume after seek", e?.message);
}
}}
/> />
</View> </View>
<Pressable <Pressable