auto timestamp lyricks when song selected
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
exports.SUNO_API_BASE = "https://api.sunoapi.org";
|
||||
exports.SUNO_API_PATH = "/api/v1/generate";
|
||||
exports.SUNO_STATUS_PATH = "/api/v1/generate/record-info";
|
||||
exports.SUNO_TIMESTAMPED_LYRICS_PATH =
|
||||
"/api/v1/generate/get-timestamped-lyrics";
|
||||
exports.SUNO_MODEL = "V4_5";
|
||||
exports.SUNO_CALLBACK_URL =
|
||||
"https://us-central1-musicland-d33f9.cloudfunctions.net/music-sunoCallback";
|
||||
+4
-1
@@ -4,9 +4,12 @@ const admin = require("firebase-admin");
|
||||
// Avoid bundling a service account key and hardcoding project/bucket.
|
||||
admin.initializeApp();
|
||||
|
||||
// Les fonctions IA ont été déplacées vers helpers/gemini
|
||||
exports.refList = {
|
||||
projects: admin.firestore().collection("projects"),
|
||||
};
|
||||
|
||||
// Exporter toutes les fonctions
|
||||
exports.music = require("./src/music");
|
||||
exports.lyrics = require("./src/lyrics");
|
||||
exports.cover = require("./src/cover");
|
||||
exports.projects = require("./src/project");
|
||||
|
||||
+98
-5
@@ -1,6 +1,14 @@
|
||||
const { onCall } = require("firebase-functions/v2/https");
|
||||
const { z } = require("genkit");
|
||||
const { generateAI, analyseLyrics } = require("../helpers/gemini");
|
||||
const {
|
||||
SUNO_API_BASE,
|
||||
SUNO_TIMESTAMPED_LYRICS_PATH,
|
||||
} = require("../config/suno");
|
||||
const { SUNO_API_KEY } = require("../config/keys");
|
||||
const axios = require("axios");
|
||||
const admin = require("firebase-admin");
|
||||
const { refList } = require("../index");
|
||||
|
||||
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
try {
|
||||
@@ -101,14 +109,25 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
lyrics: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.object({ couplet: z.string().optional(), refrain: z.string().optional() }),
|
||||
z.array(z.object({ type: z.string().optional(), lyrics: z.string().optional() })),
|
||||
z.object({
|
||||
couplet: z.string().optional(),
|
||||
refrain: z.string().optional(),
|
||||
}),
|
||||
z.array(
|
||||
z.object({
|
||||
type: z.string().optional(),
|
||||
lyrics: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
])
|
||||
.describe("Paroles à analyser (formats supportés)"),
|
||||
});
|
||||
|
||||
const parsed = requestSchema.parse(data || {});
|
||||
const result = await analyseLyrics({ title: parsed.title || "", lyrics: parsed.lyrics });
|
||||
const result = await analyseLyrics({
|
||||
title: parsed.title || "",
|
||||
lyrics: parsed.lyrics,
|
||||
});
|
||||
|
||||
// Prépare un message lisible pour le front
|
||||
const highCats = Object.entries(result.categories || {})
|
||||
@@ -140,8 +159,82 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
return {
|
||||
success: false,
|
||||
errorCode: "ANALYSE_FAILED",
|
||||
message:
|
||||
"Erreur lors de l'analyse de la toxicité. Réessayez plus tard.",
|
||||
message: "Erreur lors de l'analyse de la toxicité. Réessayez plus tard.",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Récupère les timestamps (aligned words) pour une génération Suno
|
||||
* Attend: { projectId: string }
|
||||
*/
|
||||
async function getSunoTimestamps(projectId) {
|
||||
try {
|
||||
const { sunoTaskId, songIndex } = (
|
||||
await refList.projects.doc(projectId).get()
|
||||
).data();
|
||||
if (!sunoTaskId) {
|
||||
throw new Error("TaskId manquant");
|
||||
}
|
||||
|
||||
if (songIndex < 0) {
|
||||
throw new Error("musicIndex invalide");
|
||||
}
|
||||
|
||||
if (!projectId || typeof projectId !== "string" || !projectId.trim()) {
|
||||
throw new Error("projectId manquant ou invalide");
|
||||
}
|
||||
|
||||
console.log("🔎 Récupération des timestamps Suno", {
|
||||
sunoTaskId,
|
||||
songIndex,
|
||||
});
|
||||
|
||||
let response;
|
||||
let parsed;
|
||||
|
||||
response = await axios.post(
|
||||
`${SUNO_API_BASE}${SUNO_TIMESTAMPED_LYRICS_PATH}`,
|
||||
{ taskId: sunoTaskId, musicIndex: songIndex },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${SUNO_API_KEY}`,
|
||||
},
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
parsed = response.data;
|
||||
console.log("📊 Réponse timestamps Suno API:", parsed?.code, parsed?.msg);
|
||||
|
||||
const dataToReturn = parsed?.data || parsed || {};
|
||||
|
||||
await refList.projects.doc(projectId).set(
|
||||
{
|
||||
musicTimestamps: {
|
||||
[songIndex]: dataToReturn,
|
||||
},
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
console.log("💾 Timestamps sauvegardés dans Firestore", {
|
||||
projectId,
|
||||
songIndex,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur interne getSunoTimestamps:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
type: "INTERNAL_ERROR",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exports.getSunoTimestamps = getSunoTimestamps;
|
||||
|
||||
+21
-148
@@ -3,23 +3,17 @@ const axios = require("axios");
|
||||
const admin = require("firebase-admin");
|
||||
const { logger } = require("firebase-functions/logger");
|
||||
const { SUNO_API_KEY } = require("../config/keys");
|
||||
const {
|
||||
SUNO_MODEL,
|
||||
SUNO_CALLBACK_URL,
|
||||
SUNO_API_BASE,
|
||||
SUNO_API_PATH,
|
||||
SUNO_STATUS_PATH,
|
||||
} = require("../config/suno");
|
||||
|
||||
// Initialiser Firestore
|
||||
// Initialiser Firestorey
|
||||
const db = admin.firestore();
|
||||
|
||||
/**
|
||||
* ===== CONFIG GLOBALE =====
|
||||
*/
|
||||
const SUNO_API_BASE = "https://api.sunoapi.org";
|
||||
const SUNO_API_PATH = "/api/v1/generate";
|
||||
const SUNO_STATUS_PATH = "/api/v1/generate/record-info";
|
||||
const SUNO_TIMESTAMPED_LYRICS_PATH = "/api/v1/generate/get-timestamped-lyrics";
|
||||
const SUNO_MODEL = "V4_5";
|
||||
// const SUNO_MODEL = "V3_5";
|
||||
const SUNO_CALLBACK_URL =
|
||||
"https://us-central1-musicland-d33f9.cloudfunctions.net/music-sunoCallback";
|
||||
// "https://music-sunocallback-ifdrkc2c3a-uc.a.run.app";
|
||||
|
||||
/**
|
||||
* Limite la longueur d'une chaîne
|
||||
* @param {string} str - La chaîne à limiter
|
||||
@@ -45,7 +39,9 @@ function extractVoiceStrings(voice) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (typeof voice === "object") {
|
||||
return Object.values(voice).filter((v) => typeof v === "string" && v.trim());
|
||||
return Object.values(voice).filter(
|
||||
(v) => typeof v === "string" && v.trim(),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -57,14 +53,17 @@ function voiceTagsFromUserChoice(voiceInput = "") {
|
||||
const v = String(txt || "").toLowerCase();
|
||||
|
||||
// Gender / ensemble
|
||||
if (/féminin|feminin|féminine|feminine/.test(v)) tags.push("Female lead vocal");
|
||||
if (/féminin|feminin|féminine|feminine/.test(v))
|
||||
tags.push("Female lead vocal");
|
||||
if (/masculin|masculine/.test(v)) tags.push("Male lead vocal");
|
||||
if (/deux voix|duo|deux chanteurs/.test(v)) tags.push("Duet (male and female voices)");
|
||||
if (/deux voix|duo|deux chanteurs/.test(v))
|
||||
tags.push("Duet (male and female voices)");
|
||||
if (/choeu?r|gospel/.test(v)) tags.push("Gospel choir vocals");
|
||||
|
||||
// Delivery/techniques
|
||||
if (/rap\b/.test(v)) tags.push("Rap vocal");
|
||||
if (/slam|parl[ée]e|chant parl[ée]/.test(v)) tags.push("Spoken word / slam");
|
||||
if (/slam|parl[ée]e|chant parl[ée]/.test(v))
|
||||
tags.push("Spoken word / slam");
|
||||
if (/raconte|narrat/.test(v)) tags.push("Narration / spoken narrator");
|
||||
if (/cri|scream|rugueu|growl/.test(v)) tags.push("Screamed vocals");
|
||||
if (/sprech/.test(v)) tags.push("Sprechgesang (sung-spoken) style");
|
||||
@@ -93,7 +92,10 @@ function voiceTagsFromUserChoice(voiceInput = "") {
|
||||
function detectVocalGender(voiceInput = "") {
|
||||
// If array of objects with category, prefer 'base'
|
||||
if (Array.isArray(voiceInput)) {
|
||||
const baseItem = voiceInput.find((v) => v && (v.category === "base" || v?.category?.toLowerCase() === "base"));
|
||||
const baseItem = voiceInput.find(
|
||||
(v) =>
|
||||
v && (v.category === "base" || v?.category?.toLowerCase() === "base"),
|
||||
);
|
||||
const text = baseItem ? baseItem.value || baseItem.text || baseItem : null;
|
||||
if (text) return detectVocalGender(text);
|
||||
// Fallback: scan all
|
||||
@@ -430,135 +432,6 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Récupère les timestamps (aligned words) pour une génération Suno
|
||||
* Attend: { taskId: string, musicIndex: number, projectId: string }
|
||||
*/
|
||||
exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
|
||||
try {
|
||||
const { taskId, musicIndex, projectId } = data || {};
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error("TaskId manquant");
|
||||
}
|
||||
|
||||
const index = Number(musicIndex);
|
||||
if (!Number.isFinite(index) || index < 0) {
|
||||
throw new Error("musicIndex invalide");
|
||||
}
|
||||
|
||||
if (!projectId || typeof projectId !== "string" || !projectId.trim()) {
|
||||
throw new Error("projectId manquant ou invalide");
|
||||
}
|
||||
|
||||
console.log("🔎 Récupération des timestamps Suno", {
|
||||
taskId,
|
||||
musicIndex: index,
|
||||
});
|
||||
|
||||
let response;
|
||||
let parsed;
|
||||
|
||||
try {
|
||||
response = await axios.post(
|
||||
`${SUNO_API_BASE}${SUNO_TIMESTAMPED_LYRICS_PATH}`,
|
||||
{ taskId, musicIndex: index },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${SUNO_API_KEY}`,
|
||||
},
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
parsed = response.data;
|
||||
console.log("📊 Réponse timestamps Suno API:", parsed?.code, parsed?.msg);
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur API Suno Timestamps:", {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
headers: error.config?.headers
|
||||
? { ...error.config.headers, Authorization: "[redacted]" }
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const errorStatus = error.response?.status || "UNKNOWN";
|
||||
const errorMessage =
|
||||
error.response?.data?.msg ||
|
||||
error.response?.data?.message ||
|
||||
error.response?.statusText ||
|
||||
error.message;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
taskId,
|
||||
musicIndex: index,
|
||||
error: {
|
||||
status: errorStatus,
|
||||
message: errorMessage,
|
||||
type: "API_ERROR",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dataToReturn = parsed?.data || parsed || {};
|
||||
|
||||
// Sauvegarde obligatoire dans le document projet
|
||||
let saved = false;
|
||||
try {
|
||||
await db
|
||||
.collection("projects")
|
||||
.doc(projectId)
|
||||
.set(
|
||||
{
|
||||
musicTimestamps: {
|
||||
[musicIndex]: dataToReturn,
|
||||
},
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
saved = true;
|
||||
console.log("💾 Timestamps sauvegardés dans Firestore", {
|
||||
projectId,
|
||||
index,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"❌ Échec sauvegarde timestamps Firestore:",
|
||||
e?.message || e,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
musicIndex: index,
|
||||
projectId,
|
||||
saved,
|
||||
musicTimestamps: dataToReturn,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur interne getSunoTimestamps:", error);
|
||||
return {
|
||||
success: false,
|
||||
taskId: data?.taskId,
|
||||
musicIndex: data?.musicIndex,
|
||||
error: {
|
||||
message: error.message,
|
||||
type: "INTERNAL_ERROR",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Removed Firestore onUpdate trigger for timestamps: using callable instead.
|
||||
|
||||
/**
|
||||
* Cloud Function pour recevoir les callbacks de l'API Suno
|
||||
* Cette fonction est appelée par l'API Suno lorsque la génération
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const { onDocumentWritten } = require("firebase-functions/firestore");
|
||||
const { getSunoTimestamps } = require("./lyrics");
|
||||
|
||||
exports.onProjectUpdate = onDocumentWritten(
|
||||
"projects/{projectId}",
|
||||
async (projectSnap) => {
|
||||
try {
|
||||
const { projectId } = projectSnap.params;
|
||||
|
||||
const currentData = projectSnap?.data?.after?.data() || null;
|
||||
const previousData = projectSnap?.data?.before?.data() || null;
|
||||
|
||||
if (!previousData?.songUrl && currentData?.songUrl) {
|
||||
await getSunoTimestamps(projectId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -27,9 +27,9 @@ if (!firebase?.apps?.filter(({ name_ }) => name_ === "[DEFAULT]").length) {
|
||||
persistence: getReactNativePersistence(AsyncStorage),
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
firebase.functions().useEmulator("localhost", 5001);
|
||||
}
|
||||
// if (__DEV__) {
|
||||
// firebase.functions().useEmulator("localhost", 5001);
|
||||
// }
|
||||
console.log("Firebase init");
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ const SongReady = () => {
|
||||
1: { pos: 0, dur: 0 },
|
||||
});
|
||||
const player0 = useAudioPlayer(
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
||||
);
|
||||
const player1 = useAudioPlayer(
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
|
||||
);
|
||||
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
|
||||
|
||||
@@ -120,26 +120,6 @@ const SongReady = () => {
|
||||
songUrl: url,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
// Déclenche la génération des timestamps immédiatement après avoir fixé la songUrl
|
||||
try {
|
||||
console.log("task id is : ", selectedProject?.sunoTaskId);
|
||||
const taskId = selectedProject?.sunoTaskId || null;
|
||||
if (taskId) {
|
||||
const callable = firebase
|
||||
.functions()
|
||||
.httpsCallable("music-getSunoTimestamps");
|
||||
// Fire-and-forget (ne bloque pas l'UI)
|
||||
callable({
|
||||
taskId,
|
||||
musicIndex: Number(selectedIndex) || 0,
|
||||
projectId,
|
||||
}).catch((e) =>
|
||||
console.log("getSunoTimestamps (non-blocking)", e?.message)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("getSunoTimestamps init error", e?.message);
|
||||
}
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
navigate(Routes.FlowSelection);
|
||||
@@ -157,7 +137,7 @@ const SongReady = () => {
|
||||
player1?.pause?.();
|
||||
} catch {}
|
||||
};
|
||||
}, [player0, player1])
|
||||
}, [player0, player1]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -241,7 +221,7 @@ const SongReady = () => {
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"SongReady pause on seek start",
|
||||
e?.message
|
||||
e?.message,
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user