end of cover flow. Todo: fix combine pictures and save path in storage
This commit is contained in:
+102
-134
@@ -1,123 +1,86 @@
|
||||
const { onDocumentCreated } = require("firebase-functions/v2/firestore");
|
||||
const admin = require("firebase-admin");
|
||||
const logger = require("firebase-functions/logger");
|
||||
const { generateImage } = require("../index");
|
||||
const {
|
||||
generateImageV2,
|
||||
CombineCoverAndPicture,
|
||||
} = require("../helpers/gemini");
|
||||
const { generatePicturePrompt } = require("../helpers/prompts");
|
||||
|
||||
const db = admin.firestore();
|
||||
|
||||
// Shared core for generating and saving the cover, and updating the project
|
||||
async function performCoverGeneration(projectId) {
|
||||
async function performCoverGeneration(project) {
|
||||
const t0 = Date.now();
|
||||
const snap = await db.collection("projects").doc(projectId).get();
|
||||
if (!snap.exists) {
|
||||
throw new Error("Projet introuvable");
|
||||
}
|
||||
const project = snap.data() || {};
|
||||
|
||||
const title = project?.title || "";
|
||||
const lyricsSections = Array.isArray(project?.lyrics)
|
||||
? project.lyrics
|
||||
: [
|
||||
project?.lyrics?.couplet
|
||||
? { type: "couplet", lyrics: project.lyrics.couplet }
|
||||
: null,
|
||||
project?.lyrics?.refrain
|
||||
? { type: "refrain", lyrics: project.lyrics.refrain }
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
const cfg = project?.musicConfig || {};
|
||||
const genres = Array.isArray(cfg?.genres) ? cfg.genres : [];
|
||||
const tempo = cfg?.tempo || "";
|
||||
const voice = cfg?.voice || "";
|
||||
const instruments = Array.isArray(cfg?.instruments) ? cfg.instruments : [];
|
||||
|
||||
// Construire un bref contexte visuel à partir des paroles
|
||||
const sampleLyrics = (lyricsSections || [])
|
||||
.map((s) => (s?.lyrics || "").split("\n").slice(0, 2).join(" "))
|
||||
.filter(Boolean)
|
||||
.slice(0, 6)
|
||||
.join(" | ");
|
||||
|
||||
const ambianceParts = [];
|
||||
if (genres.length) ambianceParts.push(`Genres: ${genres.join(", ")}`);
|
||||
if (tempo) ambianceParts.push(`Tempo: ${tempo}`);
|
||||
if (instruments.length)
|
||||
ambianceParts.push(`Instruments: ${instruments.join(", ")}`);
|
||||
if (voice) ambianceParts.push(`Vocal: ${voice}`);
|
||||
const ambiance = ambianceParts.join(" ; ");
|
||||
|
||||
const system = `
|
||||
Tu es un générateur qui ne renvoie que des données texte. Quand on te demande une image, tu renvoies UNIQUEMENT les octets de l'image encodés en base64, sans aucun autre texte, sans JSON, sans balises, sans préfixe data:.
|
||||
`.trim();
|
||||
|
||||
const prompt = `
|
||||
Crée une image de pochette d'album 1024x1024, moderne, abstraite et impactante, cohérente avec:
|
||||
- Titre: ${title || "Sans titre"}
|
||||
- Ambiance / Style: ${ambiance}
|
||||
|
||||
Contraintes:
|
||||
- Style abstrait: formes géométriques, textures, dégradés, couleurs harmonieuses
|
||||
- Aucun texte, aucun logo, aucune personne ni visage
|
||||
- Image PNG, sans typographie apparente
|
||||
`.trim();
|
||||
|
||||
// Utiliser generateImage qui renvoie directement le base64 (sans préfixe data:)
|
||||
logger.info("🎨 [Cover] Calling model", { projectId });
|
||||
let b64 = "";
|
||||
let mimeType = "image/png";
|
||||
const prompt = generatePicturePrompt(project);
|
||||
// Utiliser generateImageV2 qui renvoie directement l'URL publique
|
||||
logger.info("🎨 [Cover] Calling model V2", { projectId: project.id });
|
||||
let coverUrl = "";
|
||||
try {
|
||||
const ai = await generateImage({ system, prompt });
|
||||
if (typeof ai === "string") {
|
||||
b64 = ai.trim();
|
||||
} else if (ai && typeof ai === "object") {
|
||||
if (typeof ai.imageUrl === "string") {
|
||||
b64 = ai.imageUrl.replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, "").trim();
|
||||
} else if (typeof ai.b64 === "string") {
|
||||
b64 = ai.b64.trim();
|
||||
if (typeof ai.mimeType === "string") mimeType = ai.mimeType;
|
||||
}
|
||||
}
|
||||
// Utiliser une taille fixe de 1024 pixels
|
||||
coverUrl = await generateImageV2(prompt, 1024);
|
||||
console.log("coverUrl", coverUrl);
|
||||
} catch (e) {
|
||||
logger.error("❌ [Cover] generateImage failed", e);
|
||||
logger.error("❌ [Cover] generateImageV2 failed", e);
|
||||
throw e;
|
||||
}
|
||||
if (!b64) {
|
||||
throw new Error("Génération d'image échouée (base64 vide)");
|
||||
if (!coverUrl) {
|
||||
throw new Error("Génération d'image échouée (URL vide)");
|
||||
}
|
||||
|
||||
// Sauvegarde dans Cloud Storage
|
||||
const buffer = Buffer.from(b64, "base64");
|
||||
const bucket = admin.storage().bucket();
|
||||
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType === "image/webp" ? "webp" : "png";
|
||||
const path = `musics/${projectId}/cover.${ext}`;
|
||||
const token = require("crypto").randomUUID();
|
||||
|
||||
await bucket.file(path).save(buffer, {
|
||||
resumable: false,
|
||||
metadata: {
|
||||
contentType: mimeType || "image/png",
|
||||
cacheControl: "public, max-age=31536000",
|
||||
metadata: { firebaseStorageDownloadTokens: token },
|
||||
},
|
||||
});
|
||||
|
||||
const coverUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||
path,
|
||||
)}?alt=media&token=${token}`;
|
||||
|
||||
// Mettre à jour le projet avec l'URL et le statut
|
||||
await db.collection("projects").doc(projectId).set(
|
||||
await db
|
||||
.collection("projects")
|
||||
.doc(project.id)
|
||||
.set(
|
||||
{
|
||||
cover: {
|
||||
generatedBackground: coverUrl,
|
||||
},
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
logger.info("✅ [Cover] Saved", {
|
||||
projectId: project.id,
|
||||
coverUrl,
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
const combinedUrl = await CombineCoverAndPicture(background, foreground);
|
||||
|
||||
// Sauvegarder le résultat et marquer comme généré
|
||||
await db.collection("projects").doc(project.id).set(
|
||||
{
|
||||
coverUrl,
|
||||
"cover.result": combinedUrl,
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
logger.info("✅ [Cover] Saved", { projectId, path, ms: Date.now() - t0 });
|
||||
return coverUrl;
|
||||
logger.info("✅ [Cover] Combined image saved", {
|
||||
projectId: project.id,
|
||||
combinedUrl,
|
||||
ms: Date.now() - t0,
|
||||
});
|
||||
|
||||
return combinedUrl;
|
||||
}
|
||||
|
||||
// Firestore trigger: création d'une tâche de génération de cover
|
||||
@@ -128,49 +91,54 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
document: "tasks/{taskId}",
|
||||
},
|
||||
async (event) => {
|
||||
const data = event?.data?.data() || {};
|
||||
const type = data?.type || "";
|
||||
const projectId = data?.projectId || null;
|
||||
let coverUrl = null;
|
||||
try {
|
||||
const data = event?.data?.data() || {};
|
||||
const type = data?.type || "";
|
||||
const projectId = data?.projectId || null;
|
||||
|
||||
if (type !== "cover") {
|
||||
logger.info("ℹ️ [Task] Ignored non-cover task", { type });
|
||||
return;
|
||||
if (!projectId) {
|
||||
throw new Error("projectId manquant dans la task");
|
||||
}
|
||||
if (!projectId) throw new Error("projectId manquant dans la task");
|
||||
if (!["cover", "combine"]?.includes(type)) {
|
||||
throw new Error(`Type de tâche non supporté: ${type}`);
|
||||
}
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
coverStatus: "GENERATING",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
logger.info("🎨 [Task] Start cover generation", { projectId });
|
||||
const project =
|
||||
(await db.collection("projects").doc(projectId).get())?.data() || null;
|
||||
if (!project) {
|
||||
throw new Error("Projet non trouvé");
|
||||
}
|
||||
project.id = projectId;
|
||||
|
||||
// Marquer le projet en génération
|
||||
await db.collection("projects").doc(projectId).set(
|
||||
{
|
||||
coverStatus: "GENERATING",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
if (type === "cover") {
|
||||
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 });
|
||||
}
|
||||
|
||||
const coverUrl = await performCoverGeneration(projectId);
|
||||
|
||||
// Marquer la task comme terminée
|
||||
await event.data.ref.set(
|
||||
{
|
||||
status: "DONE",
|
||||
coverUrl,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
logger.info("✅ [Task] Cover generated", { projectId });
|
||||
await event.data.ref.update({
|
||||
status: "DONE",
|
||||
coverUrl,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.error("❌ [Task] Cover generation error", error);
|
||||
try {
|
||||
await event.data.ref.set(
|
||||
{ status: "ERROR", error: error?.message || "Erreur" },
|
||||
{ merge: true },
|
||||
);
|
||||
} catch (_) {}
|
||||
await event.data.ref.set(
|
||||
{ status: "ERROR", error: error?.message || "Erreur" },
|
||||
{ merge: true },
|
||||
);
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
coverStatus: "ERROR",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user