end of lyricks and misic generation

This commit is contained in:
Thomas Demirdjian
2025-08-26 13:45:50 +02:00
parent 2a34e9d547
commit 0ca7544005
14 changed files with 641 additions and 375 deletions
View File
+163 -169
View File
@@ -329,174 +329,168 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
* Cette fonction est appelée par l'API Suno lorsque la génération
* de musique est terminée
*/
exports.sunoCallback = onRequest(
{
methods: ["POST"],
},
async (req, res) => {
try {
console.log("🎵 Received music generation callback body:", req.body);
if (req.method !== "POST") {
console.warn("⚠️ Méthode non autorisée:", req.method);
return res.status(405).json({ error: "Méthode non autorisée" });
}
exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
try {
console.log("🎵 [SunoCallback] Reçu:", JSON.stringify(req.body));
const { code, msg, data } = req.body || {};
console.log("🎵 Callback details:", { code, msg, data });
// Extraire les données du callback - peut être dans data ou directement dans req.body
const callbackData = data || req.body || {};
const {
id,
taskId,
status,
audio_url: audioUrl,
video_url: videoUrl,
image_url: imageUrl,
lyric,
title,
tags,
duration,
error_message: errorMessage,
} = callbackData;
// Normaliser le statut global et le taskId
const overallStatus = status || code || callbackData?.status || null;
const overallTaskId = callbackData?.taskId || callbackData?.task_id || null;
// Déterminer les éléments piste(s) à traiter
const items = Array.isArray(callbackData?.data)
? callbackData.data
: Array.isArray(callbackData?.response?.data)
? callbackData.response.data
: Array.isArray(callbackData?.sunoData)
? callbackData.sunoData
: id || audioUrl || imageUrl || title || tags || duration
? [callbackData]
: [];
if (!items.length) {
// Aucun contenu piste à mettre à jour: acquitter le callback sans erreur
return res.status(200).json({
success: true,
message: "Callback reçu (aucune piste à mettre à jour)",
taskId: overallTaskId,
status: overallStatus,
});
}
// Retrouver l'association taskId -> projectId via le champ sunoTaskId du projet
let mappedProjectId = null;
try {
if (overallTaskId) {
const projSnap = await db
.collection("projects")
.where("sunoTaskId", "==", overallTaskId)
.limit(1)
.get();
if (!projSnap.empty) {
mappedProjectId = projSnap.docs[0].id;
}
}
} catch (e) {
// ignore mapping error, we'll proceed without projectId
}
const updates = [];
for (const item of items) {
const trackId = item.id || item.musicId || item.audioId || item.audio_id;
if (!trackId) {
continue;
}
// Chercher le document correspondant dans Firestore
const musicRef = db.collection("music").doc(trackId);
const musicDoc = await musicRef.get();
const updateData = {
status: item.status || overallStatus,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
};
const audioU = item.audio_url || item.audioUrl || item.streamAudioUrl;
const videoU = item.video_url || item.videoUrl;
const imageU = item.image_url || item.imageUrl;
const lyricText = item.lyric || item.prompt;
const titleText = item.title;
const tagsText = item.tags;
const durationVal = item.duration;
const errMsg = item.error_message || item.errorMessage;
if (audioU) updateData.audioUrl = audioU;
if (videoU) updateData.videoUrl = videoU;
if (imageU) updateData.imageUrl = imageU;
if (lyricText) updateData.lyrics = lyricText;
if (titleText) updateData.title = titleText;
if (tagsText) updateData.style = tagsText;
if (durationVal) updateData.duration = durationVal;
if (errMsg) updateData.errorMessage = errMsg;
if (overallTaskId) updateData.taskId = overallTaskId;
if (mappedProjectId) updateData.projectId = mappedProjectId;
if (!musicDoc.exists) {
updates.push(
musicRef
.set(
{
...updateData,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
)
.then(() => ({ id: trackId, ok: true }))
.catch(() => ({ id: trackId, ok: false })),
);
} else {
updates.push(
musicRef
.update(updateData)
.then(() => ({ id: trackId, ok: true }))
.catch(() => ({ id: trackId, ok: false })),
);
}
}
const results = await Promise.all(updates);
const updated = results.filter((r) => r.ok).map((r) => r.id);
// Mettre à jour le statut du projet si on connaît le projectId
if (
mappedProjectId &&
(overallStatus === 200 ||
overallStatus === "SUCCESS" ||
callbackData?.callbackType === "complete")
) {
try {
await db.collection("projects").doc(mappedProjectId).set(
{
musicStatus: "GENERATED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
} catch (_) {}
}
return res.status(200).json({
success: true,
message: "Callback traité avec succès",
updated,
taskId: overallTaskId,
status: overallStatus,
});
} catch (error) {
logger.error("❌ Erreur lors du traitement du callback:", error);
res.status(500).json({
error: "Erreur interne du serveur",
message: error.message,
});
if (req.method !== "POST") {
console.warn("⚠️ [SunoCallback] Méthode non autorisée:", req.method);
return res.status(405).json({ error: "Méthode non autorisée" });
}
},
);
const body = req.body || {};
const code = body.code ?? body.statusCode ?? null;
const callbackType = (body?.data?.callbackType || "").toString().toLowerCase();
const status = (body.status || body.state || callbackType)
.toString()
.toLowerCase();
const taskId =
body.taskId ||
body.task_id ||
body?.data?.taskId ||
body?.data?.task_id ||
null;
const tracks = Array.isArray(body?.data?.data)
? body.data.data
: Array.isArray(body.data)
? body.data
: [];
console.log("🎯 [SunoCallback] Détails:", {
code,
status,
callbackType,
taskId,
count: tracks.length,
});
// On n'agit que si code === 200 et status === "complete"
if (code !== 200 || status !== "complete") {
console.log("️ [SunoCallback] Callback ignoré (code/status)", {
code,
status,
});
return res.status(200).json({ success: true, ignored: true });
}
if (!taskId) {
console.warn("⚠️ [SunoCallback] taskId manquant dans le callback");
return res.status(200).json({ success: true, ignored: true });
}
// 1) Récupérer le projectId associé au taskId
let projectId = null;
try {
const projSnap = await db
.collection("projects")
.where("sunoTaskId", "==", taskId)
.limit(1)
.get();
if (!projSnap.empty) {
projectId = projSnap.docs[0].id;
}
} catch (e) {
console.error("❌ [SunoCallback] Erreur lookup project par taskId:", e);
}
if (!projectId) {
console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId });
return res.status(200).json({ success: true, ignored: true });
}
// 2) Extraire jusqu'à 2 URLs audio
const audioUrls = tracks
.map(
(t) =>
t.audio_url ||
t.audioUrl ||
t.stream_audio_url ||
t.streamAudioUrl,
)
.filter(Boolean)
.slice(0, 2);
if (audioUrls.length < 2) {
console.warn(
"⚠️ [SunoCallback] Moins de 2 pistes audio dans le callback",
{
found: audioUrls.length,
tracksSample: tracks.map((t) => ({
id: t.id,
has_audio_url: !!t.audio_url,
has_stream_audio_url: !!t.stream_audio_url,
})),
},
);
}
// 3) Télécharger et sauvegarder dans Cloud Storage + récupérer download URLs
const bucket = admin.storage().bucket();
console.log("🪣 [SunoCallback] Bucket:", bucket.name);
const saveOne = async (url, index) => {
if (!url) return null;
try {
console.log(`⬇️ [SunoCallback] Téléchargement piste ${index + 1}`);
const resp = await axios.get(url, { responseType: "arraybuffer" });
const buffer = Buffer.from(resp.data);
const path = `musics/${projectId}/sound${index + 1}.mp3`;
const token = require("crypto").randomUUID();
const file = bucket.file(path);
await file.save(buffer, {
resumable: false,
metadata: {
contentType: "audio/mpeg",
cacheControl: "public, max-age=31536000",
metadata: { firebaseStorageDownloadTokens: token },
},
});
const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
path,
)}?alt=media&token=${token}`;
console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl);
return { path, url: downloadUrl };
} catch (e) {
console.error(
`❌ [SunoCallback] Échec save piste ${index + 1}:`,
e.message,
);
return null;
}
};
const [p1, p2] = await Promise.all([
saveOne(audioUrls[0], 0),
saveOne(audioUrls[1], 1),
]);
// 4) Mettre à jour le statut du projet
try {
const musicUrls = [p1?.url, p2?.url].filter(Boolean);
await db.collection("projects").doc(projectId).set(
{
musicStatus: "GENERATED",
musicUrls,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
console.log("🏷️ [SunoCallback] Projet marqué GENERATED", {
projectId,
musicUrlsCount: musicUrls.length,
});
} catch (e) {
console.error("❌ [SunoCallback] Erreur maj projet:", e.message);
}
return res.status(200).json({
success: true,
projectId,
saved: [p1, p2].filter(Boolean),
});
} catch (error) {
logger.error("❌ [SunoCallback] Erreur interne:", error);
res
.status(500)
.json({ error: "Erreur interne du serveur", message: error.message });
}
});