generate two covers

This commit is contained in:
2025-10-23 10:16:36 +02:00
parent b4375d04be
commit ebe26542fe
9 changed files with 754 additions and 436 deletions
+21 -11
View File
@@ -1,5 +1,10 @@
exports.generatePicturePrompt = (project = {}) => {
const { title = "", lyrics: lyricsRaw, musicConfig = {} } = project || {};
const {
title = "",
lyrics: lyricsRaw,
musicConfig = {},
coverStyle: coverStyleRaw = "",
} = project || {};
const {
genres = [],
@@ -44,28 +49,33 @@ exports.generatePicturePrompt = (project = {}) => {
? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)"
: "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)";
const coverStyle = String(coverStyleRaw || "").trim();
const tagsLine = tags.join(" ; ") || "Non spécifié";
const lyricsLine = lyricsSample || "Pas d'extraits fournis";
const styleLine = coverStyle
? `${coverStyle}. ${paletteHint}`
: `${paletteHint}. Style libre, artistique et lumineux.`;
const prompt = `
<BRIEF>
<OBJECTIF>Créer une pochette lumineuse.</OBJECTIF>
const prompt = `<BRIEF>
<OBJECTIF>Créer une pochette d'album en adéquation avec les paroles de la musique</OBJECTIF>
<TITRE>${title || "Sans titre"}</TITRE>
<STYLE_MUSICAL>${tagsLine}</STYLE_MUSICAL>
<EXTRAITS_PAROLES>${lyricsLine}</EXTRAITS_PAROLES>
</BRIEF>
<CONTEXTES_VISUELS>
<FORMAT>Image carrée 1024x1024 pixels.</FORMAT>
<STYLE>Abstrait, graphique, formes géométriques et dégradés.</STYLE>
<COMPOSITION>Remplis toute la surface sans laisser de bordures ou zones vides.</COMPOSITION>
<FORMAT>Image carrée 1024x1024 pixels, résolution haute.</FORMAT>
<STYLE>${styleLine}</STYLE>
<COMPOSITION>La composition doit être dynamique et remplir toute la surface, sans laisser de bordures ni de zones vides.</COMPOSITION>
<TYPOGRAPHIE>Intègre de manière artistique et lisible le titre de la chanson ${title || "Sans titre"}. La typographie doit compléter le style abstrait et lumineux de la pochette. Priorise la lisibilité.</TYPOGRAPHIE>
<LOGO_RESERVE>Réserve un espace net et dégagé en bas à droite, occupant environ 25% de la largeur totale. Cette zone doit être légèrement plus sombre ou avoir un contraste suffisant.</LOGO_RESERVE>
</CONTEXTES_VISUELS>
<CONTRAINTES>
<INTERDIT>Texte ou typographie.</INTERDIT>
<INTERDIT>Personnes ou visages.</INTERDIT>
<INTERDIT>Fond blanc ou bordures.</INTERDIT>
<INTERDIT>Autre logo ou filigrane que celui prévu en bas à droite.</INTERDIT>
<INTERDIT>Personnes, visages ou silhouettes reconnaissables.</INTERDIT>
<INTERDIT>Fonds blancs ou bordures délimitant l'image.</INTERDIT>
<INTERDIT>Tout autre logo, filigrane ou texte promotionnel que le titre, l'artiste et la zone réservée pour "MusicLand Production".</INTERDIT>
<INTERDIT>Texte illisible ou trop petit.</INTERDIT>
</CONTRAINTES>
`.trim();
+74 -36
View File
@@ -75,49 +75,65 @@ async function performCoverGeneration(project) {
projectId: project.id,
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-${timestamp}.png`,
);
console.log("coverUrl", coverUrl);
} catch (e) {
logger.error(" [Cover] generateImageV2 failed", {
projectId: project.id,
error: e?.message || String(e),
const baseTimestamp = Date.now();
const options = [];
for (let index = 0; index < 2; index += 1) {
const uniqueSuffix = `${baseTimestamp}-${index}`;
const storageBasePath = `users/${project.userId}/projects/${project.id}`;
const generatedPath = `${storageBasePath}/generated-${uniqueSuffix}.png`;
let generatedUrl = "";
try {
generatedUrl = await generateImageV2(prompt, 1024, generatedPath);
logger.info("🎨 [Cover] Candidate generated", {
projectId: project.id,
candidateIndex: index,
});
} catch (e) {
logger.error("❌ [Cover] generateImageV2 failed", {
projectId: project.id,
candidateIndex: index,
error: e?.message || String(e),
});
throw e;
}
if (!generatedUrl) {
throw new Error("Génération d'image échouée (URL vide)");
}
let finalCoverUrl = generatedUrl;
try {
const stampedPath = `${storageBasePath}/cover-${uniqueSuffix}-stamped.png`;
finalCoverUrl = await buildCoverWithLogo(generatedUrl, stampedPath);
} catch (e) {
logger.error("❌ [Cover] Logo overlay failed", {
projectId: project.id,
candidateIndex: index,
error: e?.message || String(e),
});
}
options.push({
id: uniqueSuffix,
generatedUrl,
finalUrl: finalCoverUrl,
});
throw e;
}
if (!coverUrl) {
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;
}
const [firstOption] = options;
// Mettre à jour le projet avec l'URL et le statut
await db
.collection("projects")
.doc(project.id)
.set(
{
cover: {
generatedBackground: coverUrl,
result: finalCoverUrl,
generatedBackground: firstOption?.generatedUrl || null,
result: firstOption?.finalUrl || null,
selectedOptionId: firstOption?.id || null,
options,
},
coverStatus: "GENERATED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
@@ -127,11 +143,11 @@ async function performCoverGeneration(project) {
logger.info("✅ [Cover] Saved", {
projectId: project.id,
coverUrl,
finalCoverUrl,
optionsCount: options.length,
ms: Date.now() - t0,
});
return finalCoverUrl;
return firstOption?.finalUrl || null;
}
// Firestore trigger: création d'une tâche de génération de cover
@@ -185,12 +201,34 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
}
project.id = projectId;
const existingOptionsCount = Array.isArray(project?.cover?.options)
? project.cover.options.length
: 0;
logger.info("🔎 [Task] Project loaded", {
projectId,
hasGeneratedBackground: !!project?.cover?.generatedBackground,
hasUserForeground: !!project?.cover?.userForeground,
existingOptionsCount,
});
if (existingOptionsCount > 0) {
logger.warn("⛔ [Task] Cover already generated, skipping", {
projectId,
existingOptionsCount,
});
await db.collection("projects").doc(projectId).update({
coverStatus: "GENERATED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
await event.data.ref.update({
status: "CANCELLED",
error: "Cover already generated",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
return;
}
if (type === "cover") {
logger.info("🎨 [Task] Start cover generation", { projectId });
coverUrl = await performCoverGeneration(project);