re choose cover and new prompt

This commit is contained in:
2026-02-24 10:54:19 +01:00
parent e608c17775
commit b5d54226b1
8 changed files with 376 additions and 128 deletions
+61 -53
View File
@@ -2,8 +2,6 @@ const { onDocumentCreated } = require('firebase-functions/v2/firestore')
const admin = require('firebase-admin')
const { FieldValue } = require('firebase-admin/firestore')
const logger = require('firebase-functions/logger')
const path = require('path')
const fs = require('fs')
const axios = require('axios')
const sharp = require('sharp')
const crypto = require('crypto')
@@ -16,23 +14,47 @@ const { sendNotification } = require('./notifications')
// Configuration
const bucket = admin.storage().bucket()
const LOGO_PATH = path.resolve(__dirname, '../assets/musicLandLogo.png')
const BRAND_TEXT = 'By Musicland.ai'
const BRAND_FONT_FAMILY = 'Poppins, Montserrat, Arial, sans-serif'
// Cache mémoire pour le buffer du logo (évite les I/O disque à chaque appel sur instance chaude)
let _cachedLogoBuffer = null
const escapeSvgText = (value = '') =>
String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
/**
* Récupère le buffer du logo depuis le cache ou le disque
*/
const getLogoBuffer = async () => {
if (_cachedLogoBuffer) return _cachedLogoBuffer
try {
_cachedLogoBuffer = await fs.promises.readFile(LOGO_PATH)
return _cachedLogoBuffer
} catch (error) {
logger.error('❌ [Cover] Impossible de lire le fichier logo', error)
throw new Error('Asset Logo manquant sur le serveur')
}
const buildBrandSvg = ({ width = 1024, height = 1024, text = BRAND_TEXT } = {}) => {
const safeWidth = Math.max(320, Math.round(width))
const safeHeight = Math.max(320, Math.round(height))
const margin = Math.round(safeWidth * 0.04)
const fontSize = Math.max(22, Math.round(safeWidth * 0.045))
const letterSpacing = Math.max(1, Math.round(fontSize * 0.06))
const strokeWidth = Math.max(2, Math.round(fontSize * 0.08))
const shadowDy = Math.max(2, Math.round(fontSize * 0.08))
const shadowBlur = Math.max(4, Math.round(fontSize * 0.18))
const safeText = escapeSvgText(text)
return Buffer.from(`
<svg width="${safeWidth}" height="${safeHeight}" viewBox="0 0 ${safeWidth} ${safeHeight}" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="brandShadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="${shadowDy}" stdDeviation="${shadowBlur}" flood-color="rgba(0,0,0,0.55)" />
</filter>
</defs>
<text
x="${safeWidth - margin}"
y="${safeHeight - margin}"
text-anchor="end"
dominant-baseline="alphabetic"
font-family="${BRAND_FONT_FAMILY}"
font-size="${fontSize}"
font-style="italic"
font-weight="700"
letter-spacing="${letterSpacing}"
fill="rgba(255,255,255,0.95)"
stroke="rgba(0,0,0,0.45)"
stroke-width="${strokeWidth}"
paint-order="stroke"
filter="url(#brandShadow)"
>${safeText}</text>
</svg>`)
}
/**
@@ -96,39 +118,25 @@ async function resolveArtistName(project = {}) {
}
/**
* Ajoute le logo en filigrane sur l'image générée
* Ajoute le texte de marque en filigrane sur l'image générée
*/
async function buildCoverWithLogo(backgroundUrl, targetPath) {
logger.info('🖼️ [Cover] Compositing logo...')
async function buildCoverWithBrandText(backgroundUrl, targetPath) {
logger.info('🖼️ [Cover] Compositing brand text...')
try {
// Téléchargement background + Lecture Logo (parallèle)
const [bgResponse, logoBuffer] = await Promise.all([
axios.get(backgroundUrl, { responseType: 'arraybuffer' }),
getLogoBuffer(),
])
// Téléchargement background
const bgResponse = await axios.get(backgroundUrl, { responseType: 'arraybuffer' })
const baseImage = sharp(bgResponse.data)
const metadata = await baseImage.metadata()
const width = metadata.width || 1024
const height = metadata.height || 1024
// Calcul dynamique de la taille du logo (32% de la largeur)
const desiredWidth = Math.round(width * 0.32)
const margin = Math.round(width * 0.04)
// Redimensionnement du logo
const resizedLogo = await sharp(logoBuffer).resize({ width: desiredWidth }).png().toBuffer()
// Positionnement (Bas Droite)
const logoMetadata = await sharp(resizedLogo).metadata()
const left = Math.max(width - logoMetadata.width - margin, 0)
const top = Math.max(height - logoMetadata.height - margin, 0)
const brandOverlay = buildBrandSvg({ width, height, text: BRAND_TEXT })
// Composition
const stampedBuffer = await baseImage
.ensureAlpha()
.composite([{ input: resizedLogo, left, top, blend: 'over' }])
.composite([{ input: brandOverlay, left: 0, top: 0, blend: 'over' }])
.png()
.toBuffer()
@@ -147,8 +155,8 @@ async function buildCoverWithLogo(backgroundUrl, targetPath) {
return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(targetPath)}?alt=media&token=${token}`
} catch (error) {
logger.error('❌ [Cover] buildCoverWithLogo failed', error)
// En cas d'échec du logo, on renvoie l'URL originale pour ne pas tout perdre
logger.error('❌ [Cover] buildCoverWithBrandText failed', error)
// En cas d'échec du texte, on renvoie l'URL originale pour ne pas tout perdre
return backgroundUrl
}
}
@@ -160,18 +168,6 @@ async function performCoverGeneration(project) {
const t0 = Date.now()
const artistName = await resolveArtistName(project)
// Génération du Prompt optimisé
const prompt = generatePicturePrompt({
...project,
artistName,
})
logger.info('🎨 [Cover] Prompt generated', {
projectId: project.id,
artistName,
promptPreview: prompt.slice(0, 100) + '...',
})
const baseTimestamp = Date.now()
const GENERATION_COUNT = 2 // Nombre de variantes simultanées
@@ -181,6 +177,18 @@ async function performCoverGeneration(project) {
const storageBasePath = `users/${project.userId}/projects/${project.id}`
const generatedPath = `${storageBasePath}/generated-${uniqueSuffix}.png`
const stampedPath = `${storageBasePath}/cover-${uniqueSuffix}-stamped.png`
const prompt = generatePicturePrompt({
...project,
artistName,
variantSeed: `${project.id || 'project'}-${uniqueSuffix}`,
})
logger.info('🎨 [Cover] Prompt generated', {
projectId: project.id,
artistName,
variant: uniqueSuffix,
promptPreview: prompt.slice(0, 100) + '...',
})
try {
// 1. Appel IA (Imagen 3) - S'exécute en parallèle des autres
@@ -189,7 +197,7 @@ async function performCoverGeneration(project) {
if (!generatedUrl) throw new Error("URL vide retournée par l'IA")
// 2. Ajout du Logo
const finalCoverUrl = await buildCoverWithLogo(generatedUrl, stampedPath)
const finalCoverUrl = await buildCoverWithBrandText(generatedUrl, stampedPath)
logger.info(`✅ [Cover] Option ${index + 1}/${GENERATION_COUNT} ready`)