covers with logo
This commit is contained in:
+91
-54
@@ -3,11 +3,68 @@ const admin = require("firebase-admin");
|
||||
const logger = require("firebase-functions/logger");
|
||||
const {
|
||||
generateImageV2,
|
||||
CombineCoverAndPicture,
|
||||
} = require("../helpers/gemini");
|
||||
const { generatePicturePrompt } = require("../helpers/prompts");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const axios = require("axios");
|
||||
const sharp = require("sharp");
|
||||
|
||||
const db = admin.firestore();
|
||||
const bucket = admin.storage().bucket();
|
||||
const LOGO_PATH = path.resolve(
|
||||
__dirname,
|
||||
"../assets/musicLandProduction.png",
|
||||
);
|
||||
|
||||
async function buildCoverWithLogo(backgroundUrl, targetPath) {
|
||||
logger.info("🖼️ [Cover] Adding logo to generated background");
|
||||
const [{ data: backgroundBuffer }, logoBuffer] = await Promise.all([
|
||||
axios.get(backgroundUrl, { responseType: "arraybuffer" }),
|
||||
fs.promises.readFile(LOGO_PATH),
|
||||
]);
|
||||
|
||||
const baseImage = sharp(backgroundBuffer);
|
||||
const { width = 1024, height = 1024 } = await baseImage.metadata();
|
||||
|
||||
const desiredWidth = Math.round(width * 0.32);
|
||||
const margin = Math.round(width * 0.04);
|
||||
|
||||
const { data: resizedLogo, info: logoInfo } = await sharp(logoBuffer)
|
||||
.resize({ width: desiredWidth })
|
||||
.png()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
const left = Math.max(width - logoInfo.width - margin, 0);
|
||||
const top = Math.max(height - logoInfo.height - margin, 0);
|
||||
|
||||
const stampedBuffer = await baseImage
|
||||
.ensureAlpha()
|
||||
.composite([
|
||||
{
|
||||
input: resizedLogo,
|
||||
left,
|
||||
top,
|
||||
blend: "over",
|
||||
},
|
||||
])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const token = require("crypto").randomUUID();
|
||||
await bucket.file(targetPath).save(stampedBuffer, {
|
||||
resumable: false,
|
||||
metadata: {
|
||||
contentType: "image/png",
|
||||
cacheControl: "public, max-age=31536000",
|
||||
metadata: { firebaseStorageDownloadTokens: token },
|
||||
},
|
||||
});
|
||||
|
||||
return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||
targetPath,
|
||||
)}?alt=media&token=${token}`;
|
||||
}
|
||||
|
||||
// Shared core for generating and saving the cover, and updating the project
|
||||
async function performCoverGeneration(project) {
|
||||
@@ -19,12 +76,13 @@ async function performCoverGeneration(project) {
|
||||
promptPreview: String(prompt).slice(0, 160),
|
||||
});
|
||||
let coverUrl = "";
|
||||
const timestamp = Date.now();
|
||||
try {
|
||||
// Utiliser une taille fixe de 1024 pixels
|
||||
coverUrl = await generateImageV2(
|
||||
prompt,
|
||||
1024,
|
||||
`users/${project.userId}/projects/${project.id}/generated-${Date.now()}.png`,
|
||||
`users/${project.userId}/projects/${project.id}/generated-${timestamp}.png`,
|
||||
);
|
||||
console.log("coverUrl", coverUrl);
|
||||
} catch (e) {
|
||||
@@ -38,6 +96,19 @@ async function performCoverGeneration(project) {
|
||||
throw new Error("Génération d'image échouée (URL vide)");
|
||||
}
|
||||
|
||||
let finalCoverUrl = "";
|
||||
try {
|
||||
const stampedPath = `users/${project.userId}/projects/${project.id}/cover-${timestamp}-stamped.png`;
|
||||
finalCoverUrl = await buildCoverWithLogo(coverUrl, stampedPath);
|
||||
} catch (e) {
|
||||
logger.error("❌ [Cover] Logo overlay failed", {
|
||||
projectId: project.id,
|
||||
error: e?.message || String(e),
|
||||
});
|
||||
// En cas d'échec, on retient au moins la cover générée
|
||||
finalCoverUrl = coverUrl;
|
||||
}
|
||||
|
||||
// Mettre à jour le projet avec l'URL et le statut
|
||||
await db
|
||||
.collection("projects")
|
||||
@@ -46,6 +117,7 @@ async function performCoverGeneration(project) {
|
||||
{
|
||||
cover: {
|
||||
generatedBackground: coverUrl,
|
||||
result: finalCoverUrl,
|
||||
},
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
@@ -56,56 +128,10 @@ async function performCoverGeneration(project) {
|
||||
logger.info("✅ [Cover] Saved", {
|
||||
projectId: project.id,
|
||||
coverUrl,
|
||||
finalCoverUrl,
|
||||
ms: Date.now() - t0,
|
||||
});
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
async function performCombineGeneration(project) {
|
||||
const t0 = Date.now();
|
||||
const background = project?.cover?.generatedBackground || null;
|
||||
const foreground = project?.cover?.userForeground || null;
|
||||
|
||||
if (!background || !foreground) {
|
||||
throw new Error(
|
||||
"Images manquantes: cover.generatedBackground et cover.userForground sont requis",
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("🧩 [Combine] Inputs detected", {
|
||||
bgPreview: String(background).slice(0, 80),
|
||||
fgPreview: String(foreground).slice(0, 80),
|
||||
});
|
||||
|
||||
const combinedUrl = await CombineCoverAndPicture(
|
||||
background,
|
||||
foreground,
|
||||
`users/${project.userId}/projects/${project.id}/combine-${Date.now()}.png`,
|
||||
);
|
||||
|
||||
// Sauvegarder le résultat et marquer comme généré
|
||||
await db
|
||||
.collection("projects")
|
||||
.doc(project.id)
|
||||
.set(
|
||||
{
|
||||
cover: {
|
||||
...project.cover,
|
||||
result: combinedUrl,
|
||||
},
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
logger.info("✅ [Cover] Combined image saved", {
|
||||
projectId: project.id,
|
||||
combinedUrl,
|
||||
ms: Date.now() - t0,
|
||||
});
|
||||
|
||||
return combinedUrl;
|
||||
return finalCoverUrl;
|
||||
}
|
||||
|
||||
// Firestore trigger: création d'une tâche de génération de cover
|
||||
@@ -132,6 +158,21 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
type,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (type === "combine") {
|
||||
logger.warn("⛔ [Task] Combine task ignored (feature disabled)", {
|
||||
projectId,
|
||||
taskId: event?.params?.taskId,
|
||||
});
|
||||
await event.data.ref.update({
|
||||
status: "CANCELLED",
|
||||
error:
|
||||
"La personnalisation de la pochette avec une photo n'est plus disponible.",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
coverStatus: "GENERATING",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
@@ -154,10 +195,6 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
logger.info("🎨 [Task] Start cover generation", { projectId });
|
||||
coverUrl = await performCoverGeneration(project);
|
||||
logger.info("✅ [Task] Cover generated", { projectId });
|
||||
} else if (type === "combine") {
|
||||
logger.info("🧩 [Task] Start combine (cover + photo)", { projectId });
|
||||
coverUrl = await performCombineGeneration(project);
|
||||
logger.info("✅ [Task] Combine generated", { projectId, coverUrl });
|
||||
}
|
||||
|
||||
await event.data.ref.update({
|
||||
|
||||
Reference in New Issue
Block a user