fix cover generation
This commit is contained in:
+56
-17
@@ -191,23 +191,40 @@ exports.CombineCoverAndPicture = async (
|
||||
options = {},
|
||||
) => {
|
||||
const size = Number(options.size || 1024);
|
||||
console.log("🧩 [CombineCoverAndPicture] Start", { size });
|
||||
|
||||
const toDataUrl = (input, fallbackMime = "image/png") => {
|
||||
// Convertit diverses entrées (URL http(s), data URL, {base64, mimeType}) en data URL
|
||||
const toDataUrl = async (input, fallbackMime = "image/png") => {
|
||||
if (!input) return null;
|
||||
try {
|
||||
if (typeof input === "string") {
|
||||
if (input.startsWith("data:")) return input;
|
||||
// http(s) URL - laissez le middleware télécharger
|
||||
if (/^https?:\/\//i.test(input)) return input;
|
||||
// Base64 brut (non recommandé sans mimeType) -> tenter fallback
|
||||
if (/^https?:\/\//i.test(input)) {
|
||||
const axios = require("axios");
|
||||
const resp = await axios.get(input, { responseType: "arraybuffer" });
|
||||
const mime = resp.headers["content-type"] || fallbackMime;
|
||||
const b64 = Buffer.from(resp.data).toString("base64");
|
||||
return `data:${mime};base64,${b64}`;
|
||||
}
|
||||
// Base64 brut -> fallback
|
||||
if (/^[A-Za-z0-9+/=]+$/.test(input) && input.length > 256) {
|
||||
return `data:${fallbackMime};base64,${input}`;
|
||||
}
|
||||
return input; // dernier recours
|
||||
return input;
|
||||
}
|
||||
if (typeof input === "object") {
|
||||
if (input.url) {
|
||||
if (input.url.startsWith("data:")) return input.url;
|
||||
if (/^https?:\/\//i.test(input.url)) return input.url; // middleware download
|
||||
if (/^https?:\/\//i.test(input.url)) {
|
||||
const axios = require("axios");
|
||||
const resp = await axios.get(input.url, {
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
const mime =
|
||||
input.mimeType || resp.headers["content-type"] || fallbackMime;
|
||||
const b64 = Buffer.from(resp.data).toString("base64");
|
||||
return `data:${mime};base64,${b64}`;
|
||||
}
|
||||
if (input.base64)
|
||||
return `data:${input.mimeType || fallbackMime};base64,${input.base64}`;
|
||||
return input.url;
|
||||
@@ -216,11 +233,14 @@ exports.CombineCoverAndPicture = async (
|
||||
return `data:${input.mimeType || fallbackMime};base64,${input.base64}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("toDataUrl: échec de conversion en data URL", e?.message);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const bgUrl = toDataUrl(backgroundImage, "image/png");
|
||||
const fgUrl = toDataUrl(foregroundImage, "image/png");
|
||||
const bgUrl = await toDataUrl(backgroundImage, "image/png");
|
||||
const fgUrl = await toDataUrl(foregroundImage, "image/png");
|
||||
if (!bgUrl || !fgUrl) {
|
||||
throw new Error(
|
||||
"CombineCoverAndPicture: images d'entrée invalides (background/foreground requis)",
|
||||
@@ -234,23 +254,38 @@ exports.CombineCoverAndPicture = async (
|
||||
|
||||
// Instruction en FR pour un compositing propre
|
||||
const instruction = `
|
||||
Combine les deux images fournies sur une toile carrée ${size}x${size}.
|
||||
Utilise la première image comme arrière-plan en plein cadre (recadrer au besoin sans bords vides).
|
||||
Détoure précisément le sujet principal de la seconde image (suppression d'arrière-plan),
|
||||
puis place-le par-dessus, centré, avec une légère ombre portée douce pour le détacher.
|
||||
Préserve les détails/contours, couleurs naturelles, et une intégration réaliste (échelle, lumière).
|
||||
L'image générer sera une pochette d'album de musique.
|
||||
Aucun texte, aucun logo, aucun filigrane. Export en PNG.
|
||||
Tu reçois 2 images: la première est l'arrière-plan, la seconde est une photo à incruster.
|
||||
1) Crée une toile carrée ${size}x${size} et place l'arrière-plan en plein cadre, sans bords vides (recadrer si nécessaire).
|
||||
2) Détoure le sujet principal de la seconde image.
|
||||
3) Place le sujet détouré par-dessus l'arrière-plan, et essaie de faire une jolie pochette.
|
||||
4) Aucun texte, aucun logo, aucun filigrane. Export en PNG.
|
||||
`.trim();
|
||||
|
||||
// Construire le prompt avec parties media
|
||||
// Utiliser des data URLs pour garantir la prise en compte des 2 images
|
||||
const parts = [
|
||||
{ text: instruction },
|
||||
{ media: { url: bgUrl } },
|
||||
{ media: { url: fgUrl } },
|
||||
];
|
||||
|
||||
const res = await ai.generate({ model, prompt: parts });
|
||||
console.log("🧩 [CombineCoverAndPicture] Entrées prêtes", {
|
||||
bgIsDataUrl: typeof bgUrl === "string" && bgUrl.startsWith("data:"),
|
||||
fgIsDataUrl: typeof fgUrl === "string" && fgUrl.startsWith("data:"),
|
||||
bgLen: (bgUrl || "").length,
|
||||
fgLen: (fgUrl || "").length,
|
||||
});
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await ai.generate({ model, prompt: parts });
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"❌ [CombineCoverAndPicture] ai.generate failed",
|
||||
err?.message || err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
const media = res?.media;
|
||||
if (!media || !media.url) {
|
||||
throw new Error("La génération n'a pas retourné d'image composite.");
|
||||
@@ -289,6 +324,10 @@ Aucun texte, aucun logo, aucun filigrane. Export en PNG.
|
||||
const publicUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||
path,
|
||||
)}?alt=media&token=${token}`;
|
||||
|
||||
console.log("✅ [CombineCoverAndPicture] Uploaded", {
|
||||
path,
|
||||
mimeType,
|
||||
urlLen: publicUrl.length,
|
||||
});
|
||||
return publicUrl;
|
||||
};
|
||||
|
||||
+3
-7
@@ -1,12 +1,8 @@
|
||||
const admin = require("firebase-admin");
|
||||
const serviceAccount = require("./serviceAccountKey.json");
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
storageBucket: "musicland-d33f9.firebasestorage.app",
|
||||
databaseURL: "https://musicland-d33f9-default-rtdb.firebaseio.com",
|
||||
projectId: "musicland-d33f9",
|
||||
});
|
||||
// Use default credentials/environment provided by Cloud Functions.
|
||||
// Avoid bundling a service account key and hardcoding project/bucket.
|
||||
admin.initializeApp();
|
||||
|
||||
// Les fonctions IA ont été déplacées vers helpers/gemini
|
||||
|
||||
|
||||
+42
-5
@@ -14,14 +14,20 @@ async function performCoverGeneration(project) {
|
||||
const t0 = Date.now();
|
||||
const prompt = generatePicturePrompt(project);
|
||||
// Utiliser generateImageV2 qui renvoie directement l'URL publique
|
||||
logger.info("🎨 [Cover] Calling model V2", { projectId: project.id });
|
||||
logger.info("🎨 [Cover] Calling model V2", {
|
||||
projectId: project.id,
|
||||
promptPreview: String(prompt).slice(0, 160),
|
||||
});
|
||||
let coverUrl = "";
|
||||
try {
|
||||
// Utiliser une taille fixe de 1024 pixels
|
||||
coverUrl = await generateImageV2(prompt, 1024);
|
||||
console.log("coverUrl", coverUrl);
|
||||
} catch (e) {
|
||||
logger.error("❌ [Cover] generateImageV2 failed", e);
|
||||
logger.error("❌ [Cover] generateImageV2 failed", {
|
||||
projectId: project.id,
|
||||
error: e?.message || String(e),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
if (!coverUrl) {
|
||||
@@ -62,12 +68,23 @@ async function performCombineGeneration(project) {
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("🧩 [Combine] Inputs detected", {
|
||||
bgPreview: String(background).slice(0, 80),
|
||||
fgPreview: String(foreground).slice(0, 80),
|
||||
});
|
||||
|
||||
const combinedUrl = await CombineCoverAndPicture(background, foreground);
|
||||
|
||||
// Sauvegarder le résultat et marquer comme généré
|
||||
await db.collection("projects").doc(project.id).set(
|
||||
await db
|
||||
.collection("projects")
|
||||
.doc(project.id)
|
||||
.set(
|
||||
{
|
||||
"cover.result": combinedUrl,
|
||||
cover: {
|
||||
...project.cover,
|
||||
result: combinedUrl,
|
||||
},
|
||||
coverStatus: "GENERATED",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
@@ -102,6 +119,11 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
if (!["cover", "combine"]?.includes(type)) {
|
||||
throw new Error(`Type de tâche non supporté: ${type}`);
|
||||
}
|
||||
logger.info("🧵 [Task] Received", {
|
||||
taskId: event?.params?.taskId,
|
||||
type,
|
||||
projectId,
|
||||
});
|
||||
await db.collection("projects").doc(projectId).update({
|
||||
coverStatus: "GENERATING",
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
@@ -114,6 +136,12 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
}
|
||||
project.id = projectId;
|
||||
|
||||
logger.info("🔎 [Task] Project loaded", {
|
||||
projectId,
|
||||
hasGeneratedBackground: !!project?.cover?.generatedBackground,
|
||||
hasUserForeground: !!project?.cover?.userForeground,
|
||||
});
|
||||
|
||||
if (type === "cover") {
|
||||
logger.info("🎨 [Task] Start cover generation", { projectId });
|
||||
coverUrl = await performCoverGeneration(project);
|
||||
@@ -129,9 +157,18 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
||||
coverUrl,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
logger.info("📦 [Task] Marked DONE", {
|
||||
taskId: event?.params?.taskId,
|
||||
projectId,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.error("❌ [Task] Cover generation error", error);
|
||||
logger.error("❌ [Task] Cover generation error", {
|
||||
taskId: event?.params?.taskId,
|
||||
projectId,
|
||||
type,
|
||||
error: error?.message || String(error),
|
||||
});
|
||||
await event.data.ref.set(
|
||||
{ status: "ERROR", error: error?.message || "Erreur" },
|
||||
{ merge: true },
|
||||
|
||||
@@ -116,11 +116,17 @@ const PhotoCover = () => {
|
||||
>
|
||||
<BorderGradientButton
|
||||
onPress={() => navigate(Routes.ValidateCover)}
|
||||
title="Non, valider la pochette"
|
||||
title={
|
||||
isGenerating ? "Génération en cours..." : "Valider la pochette"
|
||||
}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Oui choisir une image"
|
||||
title={
|
||||
isGenerating ? "Génération en cours..." : "Oui choisir une image"
|
||||
}
|
||||
onPress={() => onChooseForegroundImage()}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
Reference in New Issue
Block a user