end of lyricks and misic generation
This commit is contained in:
Generated
+5
-4
@@ -10,7 +10,8 @@
|
||||
"axios": "^1.6.0",
|
||||
"firebase-admin": "^12.1.0",
|
||||
"firebase-functions": "^5.0.0",
|
||||
"genkit": "^1.16.0"
|
||||
"genkit": "^1.16.0",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.15.0",
|
||||
@@ -4749,9 +4750,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -18,11 +18,15 @@
|
||||
"axios": "^1.6.0",
|
||||
"firebase-admin": "^12.1.0",
|
||||
"firebase-functions": "^5.0.0",
|
||||
"genkit": "^1.16.0"
|
||||
"genkit": "^1.16.0",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
},
|
||||
"overrides": {
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
+163
-169
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user