covers with logo

This commit is contained in:
2025-10-22 17:06:49 +02:00
parent 40e10d1692
commit b4375d04be
9 changed files with 643 additions and 228 deletions
+91 -54
View File
@@ -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({