diff --git a/functions/helpers/gemini.js b/functions/helpers/gemini.js new file mode 100644 index 0000000..08c47d3 --- /dev/null +++ b/functions/helpers/gemini.js @@ -0,0 +1,294 @@ +const { googleAI } = require("@genkit-ai/googleai"); +const { genkit } = require("genkit"); +const { GEMINI_API_KEY } = require("../config/keys"); +const admin = require("firebase-admin"); +const { Buffer } = require("buffer"); +const { setTimeout } = require("timers"); + +exports.generateAI = async ({ system = "", prompt = "", schema }) => { + const genkitInstance = genkit({ + plugins: [googleAI({ apiKey: GEMINI_API_KEY })], + model: googleAI.model("gemini-2.5-flash"), + }); + + if (prompt?.length < 1) { + throw new Error( + "Vous devez spécifier un prompt pour effectuer cette action.", + ); + } + + const { output } = await genkitInstance.generate({ + system, + prompt, + output: { schema }, + }); + + return output; +}; + +exports.generateImageV2 = async (prompt, size = 1024) => { + if (typeof prompt !== "string" || prompt.trim().length < 1) { + throw new Error("Vous devez spécifier un prompt (string) non vide."); + } + + try { + console.log("🚀 [generateImageV2] Démarrage de la génération d'image", { + promptLength: prompt.trim().length, + size, + }); + + // Ajouter explicitement la taille au prompt si elle n'est pas mentionnée + if (!prompt.includes("1024x1024") && !prompt.includes(size + "x" + size)) { + prompt = `Image carrée ${size}x${size} pixels. ${prompt}`; + } + + const ai = genkit({ + plugins: [googleAI({ apiKey: GEMINI_API_KEY })], + }); + + const imageModel = googleAI.model("gemini-2.5-flash-image-preview", { + responseModalities: ["IMAGE"], + // L'API n'accepte pas generationConfig comme paramètre direct + // Les paramètres de génération doivent être passés lors de l'appel generate() + }); + + console.log("🔄 [generateImageV2] Envoi du prompt au modèle"); + + // Ajouter un timeout et des tentatives + let attempts = 0; + const maxAttempts = 3; + let res; + + while (attempts < maxAttempts) { + try { + attempts++; + console.log( + `🔁 [generateImageV2] Tentative ${attempts}/${maxAttempts}`, + ); + + // Utiliser Promise.race pour ajouter un timeout + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Timeout dépassé (30s)")), 30000); + }); + + res = await Promise.race([ + ai.generate({ + model: imageModel, + prompt: prompt.trim(), + // Paramètres de génération passés dans un objet generation_config + generation_config: { + temperature: 0.7, + top_k: 40, + top_p: 0.95, + max_output_tokens: 8192, + }, + }), + timeout, + ]); + + if (res?.media?.url) { + console.log( + "✅ [generateImageV2] Image générée avec succès à la tentative", + attempts, + ); + break; // Sortir de la boucle si on a une image + } else { + console.warn( + `⚠️ [generateImageV2] Tentative ${attempts} sans média, nouvelle tentative...`, + ); + // Attendre un peu avant de réessayer + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } catch (err) { + console.error( + `❌ [generateImageV2] Erreur à la tentative ${attempts}:`, + err.message, + ); + if (attempts >= maxAttempts) throw err; + // Attendre un peu plus longtemps après une erreur + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + } + + console.log("📊 [generateImageV2] Réponse finale", { + hasMedia: !!res?.media, + hasUrl: !!res?.media?.url, + responseKeys: Object.keys(res || {}).join(", "), + }); + + const media = res?.media; + if (!media || !media.url) { + console.error( + "❌ [generateImageV2] Pas de média dans la réponse après toutes les tentatives", + { + response: JSON.stringify(res || {}).substring(0, 500), + prompt: prompt.substring(0, 200) + "...", + }, + ); + throw new Error( + "La génération n'a pas retourné de média image après plusieurs tentatives.", + ); + } + + const dataUrl = String(media.url); + const commaIdx = dataUrl.indexOf(","); + if (commaIdx === -1) throw new Error("FORMAT_DATA_URL_INVALIDE"); + const header = dataUrl.substring(0, commaIdx); + const b64 = dataUrl.substring(commaIdx + 1); + + let mimeType = media.contentType || "image/png"; + const headerMatch = header.match(/^data:([^;]+);base64$/i); + if (headerMatch && headerMatch[1]) mimeType = headerMatch[1]; + + const buffer = Buffer.from(b64, "base64"); + const bucket = admin.storage().bucket(); + const ext = + mimeType === "image/jpeg" + ? "jpg" + : mimeType === "image/webp" + ? "webp" + : "png"; + const token = require("crypto").randomUUID(); + const path = `generated/images/${Date.now()}-${token}.${ext}`; + + console.log( + "💾 [generateImageV2] Sauvegarde de l'image dans Firebase Storage", + ); + await bucket.file(path).save(buffer, { + resumable: false, + metadata: { + contentType: mimeType, + cacheControl: "public, max-age=31536000", + metadata: { firebaseStorageDownloadTokens: token }, + }, + }); + + const publicUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent( + path, + )}?alt=media&token=${token}`; + + console.log("✅ [generateImageV2] Image générée avec succès"); + return publicUrl; + } catch (error) { + console.error( + "❌ [generateImageV2] Erreur lors de la génération d'image:", + error, + ); + throw new Error(`Erreur lors de la génération d'image: ${error.message}`); + } +}; + +// Combine deux images avec Gemini (background = cover abstraite, foreground = photo utilisateur détourée) +// Entrées possibles pour chaque image: +// - string data URL ("data:image/...;base64,....") +// - string URL http(s) +// - { base64: string, mimeType: string } +// - { url: string, mimeType?: string } +// Sortie: URL publique de l'image composite (PNG 1024x1024) +exports.CombineCoverAndPicture = async ( + backgroundImage, + foregroundImage, + options = {}, +) => { + const size = Number(options.size || 1024); + + const toDataUrl = (input, fallbackMime = "image/png") => { + if (!input) return null; + 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 (/^[A-Za-z0-9+/=]+$/.test(input) && input.length > 256) { + return `data:${fallbackMime};base64,${input}`; + } + return input; // dernier recours + } + 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 (input.base64) + return `data:${input.mimeType || fallbackMime};base64,${input.base64}`; + return input.url; + } + if (input.base64) { + return `data:${input.mimeType || fallbackMime};base64,${input.base64}`; + } + } + return null; + }; + + const bgUrl = toDataUrl(backgroundImage, "image/png"); + const fgUrl = toDataUrl(foregroundImage, "image/png"); + if (!bgUrl || !fgUrl) { + throw new Error( + "CombineCoverAndPicture: images d'entrée invalides (background/foreground requis)", + ); + } + + const ai = genkit({ plugins: [googleAI({ apiKey: GEMINI_API_KEY })] }); + const model = googleAI.model("gemini-2.5-flash-image-preview", { + responseModalities: ["IMAGE"], + }); + + // 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. +`.trim(); + + // Construire le prompt avec parties media + const parts = [ + { text: instruction }, + { media: { url: bgUrl } }, + { media: { url: fgUrl } }, + ]; + + const res = await ai.generate({ model, prompt: parts }); + const media = res?.media; + if (!media || !media.url) { + throw new Error("La génération n'a pas retourné d'image composite."); + } + + const dataUrl = String(media.url); + const commaIdx = dataUrl.indexOf(","); + if (commaIdx === -1) throw new Error("FORMAT_DATA_URL_INVALIDE"); + const header = dataUrl.substring(0, commaIdx); + const b64 = dataUrl.substring(commaIdx + 1); + + let mimeType = media.contentType || "image/png"; + const headerMatch = header.match(/^data:([^;]+);base64$/i); + if (headerMatch && headerMatch[1]) mimeType = headerMatch[1]; + + const buffer = Buffer.from(b64, "base64"); + const bucket = admin.storage().bucket(); + const ext = + mimeType === "image/jpeg" + ? "jpg" + : mimeType === "image/webp" + ? "webp" + : "png"; + const token = require("crypto").randomUUID(); + const path = `generated/combined/${Date.now()}-${token}.${ext}`; + + await bucket.file(path).save(buffer, { + resumable: false, + metadata: { + contentType: mimeType, + cacheControl: "public, max-age=31536000", + metadata: { firebaseStorageDownloadTokens: token }, + }, + }); + + const publicUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent( + path, + )}?alt=media&token=${token}`; + + return publicUrl; +}; diff --git a/functions/helpers/prompts.js b/functions/helpers/prompts.js new file mode 100644 index 0000000..013e451 --- /dev/null +++ b/functions/helpers/prompts.js @@ -0,0 +1,69 @@ +exports.generatePicturePrompt = (project = {}) => { + const { title = "", lyrics: lyricsRaw, musicConfig = {} } = project || {}; + + const { + genres = [], + tempo = "", + voice = "", + instruments = [], + mood = "", + } = musicConfig || {}; + + // Normalise les paroles en tableau de sections { type, lyrics } + const lyricsSections = Array.isArray(lyricsRaw) + ? lyricsRaw + : [ + lyricsRaw?.couplet + ? { type: "couplet", lyrics: lyricsRaw.couplet } + : null, + lyricsRaw?.refrain + ? { type: "refrain", lyrics: lyricsRaw.refrain } + : null, + ].filter(Boolean); + + // Échantillon pour l'ambiance (courtes bribes pour inspirer la direction créative) + const lyricsSample = (lyricsSections || []) + .map((s) => (s?.lyrics || "").split("\n").slice(0, 2).join(" ")) + .filter(Boolean) + .slice(0, 6) + .join(" | "); + + // Etiquettes d'ambiance + const tags = []; + if (Array.isArray(genres) && genres.length) + tags.push(`Genres: ${genres.join(", ")}`); + if (tempo) tags.push(`Tempo: ${tempo}`); + if (Array.isArray(instruments) && instruments.length) + tags.push(`Instruments: ${instruments.join(", ")}`); + if (voice) tags.push(`Vocal: ${voice}`); + if (mood) tags.push(`Mood: ${mood}`); + + // Guidance palette selon tempo (simple heuristique) + const paletteHint = + tempo && /\b(rapid|fast|vite|agité|upbeat|energ)/i.test(String(tempo)) + ? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)" + : "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)"; + + // Prompt simplifié et optimisé pour Gemini + const prompt = ` +Crée une pochette d'album musicale abstraite et colorée au format carré 1024x1024. + +Contexte: +- Titre: ${title || "Sans titre"} +- Style musical: ${tags.join(" ; ")} + +Directives essentielles: +- Style: Abstrait, graphique, avec formes géométriques et dégradés de couleurs +- Couleurs: ${paletteHint} +- IMPORTANT: Image plein cadre sans AUCUN bord blanc ou espace vide +- Composition: Éléments visuels répartis sur TOUTE la surface de l'image + +Contraintes: +- PAS de texte ou typographie +- PAS de personnes ou visages +- PAS de fond blanc ou bordures +- PAS de filigrane +`.trim(); + + return prompt; +}; diff --git a/functions/index.js b/functions/index.js index fc689ff..eaf0afa 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,134 +1,14 @@ const admin = require("firebase-admin"); const serviceAccount = require("./serviceAccountKey.json"); -const { googleAI } = require("@genkit-ai/googleai"); -const { GEMINI_API_KEY } = require("./config/keys"); -const { genkit } = require("genkit"); -const axios = require("axios"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), storageBucket: "musicland-d33f9.firebasestorage.app", + databaseURL: "https://musicland-d33f9-default-rtdb.firebaseio.com", + projectId: "musicland-d33f9", }); -exports.generateAI = async ({ system = "", prompt = "", schema }) => { - const genkitInstance = genkit({ - plugins: [ - googleAI({ - apiKey: GEMINI_API_KEY, - }), - ], - model: googleAI.model("gemini-2.5-flash"), - }); - - if (prompt?.length < 1) { - throw new Error( - "Vous devez spécifier un prompt pour effectuer cette action.", - ); - } - - const { output } = await genkitInstance.generate({ - system, - prompt, - output: { schema }, - }); - - return output; -}; - -// Générateur d'images via appel direct REST Imagen 3.0 (retour base64 pur) -exports.generateImage = async ({ system = "", prompt = "" }) => { - if (typeof prompt !== "string" || prompt.trim().length < 1) { - throw new Error( - "Vous devez spécifier un prompt pour effectuer cette action.", - ); - } - - const baseUrl = - "https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict"; - const url = `${baseUrl}?key=${encodeURIComponent(GEMINI_API_KEY)}`; - - const mkBody = (p) => ({ - instances: [ - { - prompt: p, - }, - ], - parameters: { - sampleCount: 1, - aspectRatio: "1:1", - personGeneration: "dont_allow", - }, - }); - - const combinedPrompt = [ - system, - "Contraintes supplémentaires: pas de personnes, pas de visages, style abstrait, formes géométriques, textures et motifs.", - prompt, - ] - .filter(Boolean) - .join("\n\n"); - - const tryOnce = async (p) => { - const resp = await axios.post(url, mkBody(p), { - headers: { "Content-Type": "application/json" }, - timeout: 120000, - }); - const predictions = resp?.data?.predictions || []; - const first = predictions.find((x) => x?.bytesBase64Encoded); - return first - ? { - b64: first.bytesBase64Encoded, - mimeType: first.mimeType || "image/png", - } - : { b64: "", mimeType: null }; - }; - - // Primary try - let result = { b64: "", mimeType: null }; - try { - result = await tryOnce(combinedPrompt); - } catch (e) { - const em = e?.response?.data || e?.message || e; - console.error("Imagen predict error", em); - } - - // Fallbacks on safety filters/no predictions - if (!result?.b64) { - const fallback = [ - "Crée une image de pochette d'album 1024x1024, abstraite, moderne et colorée.", - "Utilise des formes géométriques, textures et dégradés.", - "Aucune personne, aucun visage, aucun texte, aucun logo.", - ].join("\n"); - try { - result = await tryOnce(fallback); - } catch (e2) { - const em2 = e2?.response?.data || e2?.message || e2; - console.error("Imagen predict fallback FR error", em2); - } - } - - if (!result?.b64) { - const fallbackEn = [ - "Create a 1024x1024 abstract, modern, colorful album cover.", - "Use geometric shapes, textures, and gradients.", - "No people, no faces, no text, no logos.", - ].join("\n"); - try { - result = await tryOnce(fallbackEn); - } catch (e3) { - const em3 = e3?.response?.data || e3?.message || e3; - console.error("Imagen predict fallback EN error", em3); - } - } - - if (!result?.b64) { - throw new Error("NO_IMAGE_OUTPUT"); - } - return { - b64: String(result.b64).trim(), - mimeType: result.mimeType || "image/png", - }; -}; +// Les fonctions IA ont été déplacées vers helpers/gemini // Exporter toutes les fonctions exports.music = require("./src/music"); diff --git a/functions/package-lock.json b/functions/package-lock.json index 7b2dc4c..44c40d7 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -7,9 +7,9 @@ "name": "functions", "dependencies": { "@genkit-ai/googleai": "^1.18.0", - "axios": "^1.6.0", - "firebase-admin": "^12.1.0", - "firebase-functions": "^5.0.0", + "axios": "^1.11.0", + "firebase-admin": "^12.7.0", + "firebase-functions": "^6.4.0", "genkit": "^1.18.0" }, "devDependencies": { @@ -17,7 +17,7 @@ "eslint-config-google": "^0.14.0" }, "engines": { - "node": "18" + "node": "20" } }, "node_modules/@colors/colors": { @@ -43,9 +43,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", + "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -106,9 +106,9 @@ } }, "node_modules/@fastify/busboy": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", - "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "license": "MIT" }, "node_modules/@firebase/app-check-interop-types": { @@ -215,32 +215,14 @@ } }, "node_modules/@genkit-ai/ai/node_modules/@types/node": { - "version": "20.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", - "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "version": "20.19.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.12.tgz", + "integrity": "sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@genkit-ai/ai/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/@genkit-ai/core": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/@genkit-ai/core/-/core-1.18.0.tgz", @@ -343,25 +325,6 @@ "genkit": "^1.18.0" } }, - "node_modules/@genkit-ai/google-cloud/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "optional": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/@genkit-ai/googleai": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/@genkit-ai/googleai/-/googleai-1.18.0.tgz", @@ -376,24 +339,6 @@ "genkit": "^1.18.0" } }, - "node_modules/@genkit-ai/googleai/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/@google-cloud/common": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz", @@ -433,16 +378,16 @@ } }, "node_modules/@google-cloud/logging": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-11.2.0.tgz", - "integrity": "sha512-Ma94jvuoMpbgNniwtelOt8w82hxK62FuOXZonEv0Hyk3B+/YVuLG/SWNyY9yMso/RXnPEc1fP2qo9kDrjf/b2w==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-11.2.1.tgz", + "integrity": "sha512-2h9HBJG3OAsvzXmb81qXmaTPfXYU7KJTQUxunoOKFGnY293YQ/eCkW1Y5mHLocwpEqeqQYT/Qvl6Tk+Q7PfStw==", "license": "Apache-2.0", "optional": true, "dependencies": { "@google-cloud/common": "^5.0.0", "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", + "@google-cloud/promisify": "4.0.0", "@opentelemetry/api": "^1.7.0", "arrify": "^2.0.1", "dot-prop": "^6.0.0", @@ -599,9 +544,9 @@ } }, "node_modules/@google-cloud/storage": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz", - "integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.0.tgz", + "integrity": "sha512-5m9GoZqKh52a1UqkxDBu/+WVFDALNtHg5up5gNmNbXQWBcV813tzJKsyDtKjOPrlR1em1TxtD7NSPCrObH7koQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -2184,9 +2129,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz", - "integrity": "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==", + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", + "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", "license": "Apache-2.0", "optional": true, "engines": { @@ -2431,9 +2376,9 @@ } }, "node_modules/@types/node": { - "version": "22.17.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz", - "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==", + "version": "22.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", + "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2486,6 +2431,24 @@ "form-data": "^2.5.5" } }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", @@ -2764,22 +2727,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2971,43 +2918,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -3308,6 +3218,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -3893,15 +3809,15 @@ } }, "node_modules/firebase-functions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", - "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.4.0.tgz", + "integrity": "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg==", "license": "MIT", "dependencies": { "@types/cors": "^2.8.5", - "@types/express": "4.17.3", + "@types/express": "^4.17.21", "cors": "^2.8.5", - "express": "^4.17.1", + "express": "^4.21.0", "protobufjs": "^7.2.2" }, "bin": { @@ -3911,18 +3827,7 @@ "node": ">=14.10.0" }, "peerDependencies": { - "firebase-admin": "^11.10.0 || ^12.0.0" - } - }, - "node_modules/firebase-functions/node_modules/@types/express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", - "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" + "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0" } }, "node_modules/flat-cache": { @@ -3975,21 +3880,19 @@ } }, "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", - "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" + "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/formdata-polyfill": { @@ -4061,6 +3964,26 @@ "node": ">=14" } }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/gaxios/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -4157,6 +4080,28 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4227,6 +4172,27 @@ "node": ">=14" } }, + "node_modules/google-gax/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/google-gax/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -4995,17 +4961,7 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, - "node_modules/lru-memoizer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", - "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", - "license": "MIT", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" - } - }, - "node_modules/lru-memoizer/node_modules/lru-cache": { + "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", @@ -5017,11 +4973,15 @@ "node": ">=10" } }, - "node_modules/lru-memoizer/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -5170,23 +5130,21 @@ } }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/node-forge": { @@ -5505,9 +5463,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", - "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -5770,28 +5728,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6116,6 +6052,20 @@ "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6230,6 +6180,27 @@ "node": ">= 6" } }, + "node_modules/teeny-request/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/teeny-request/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -6560,6 +6531,23 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6591,6 +6579,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", @@ -6630,26 +6624,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/functions/package.json b/functions/package.json index dc55963..0a01f0b 100644 --- a/functions/package.json +++ b/functions/package.json @@ -10,14 +10,14 @@ "logs": "firebase functions:log" }, "engines": { - "node": "18" + "node": "20" }, "main": "index.js", "dependencies": { "@genkit-ai/googleai": "^1.18.0", - "axios": "^1.6.0", - "firebase-admin": "^12.1.0", - "firebase-functions": "^5.0.0", + "axios": "^1.11.0", + "firebase-admin": "^12.7.0", + "firebase-functions": "^6.4.0", "genkit": "^1.18.0" }, "devDependencies": { diff --git a/functions/src/cover.js b/functions/src/cover.js index 9faa81c..8b81899 100644 --- a/functions/src/cover.js +++ b/functions/src/cover.js @@ -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", + }); } }, ); diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js index 35091a8..1d52d6e 100644 --- a/functions/src/lyrics.js +++ b/functions/src/lyrics.js @@ -1,6 +1,6 @@ const { onCall } = require("firebase-functions/v2/https"); const { z } = require("genkit"); -const { generateAI } = require("../index"); +const { generateAI } = require("../helpers/gemini"); exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => { try { @@ -45,7 +45,7 @@ Schéma de rimes : ${rhymes} Génère un titre accrocheur et les paroles de la chanson en suivant ` + `strictement cette structure. Le titre doit être court, ` + - `mémorable et refléter l'esprit de l'agence Minuit. + `mémorable et refléter l'esprit de la musique. Pour chaque ` + `partie ("couplet" ou "refrain"), écris 4 à 16 lignes adaptées, ` + `expressives et cohérentes avec le style et l'émotion souhaitée. @@ -75,6 +75,9 @@ Pour chaque ` + "Paroles de la chanson sous forme de tableau de sections " + "structurées.", ), + lyricsDescription: z + .string() + .describe("Courte déscription qui résume les paroles de la musique"), success: z.boolean().describe("Indique si la génération a réussi"), }); diff --git a/functions/src/music.js b/functions/src/music.js index 6ed78f0..1e0f409 100644 --- a/functions/src/music.js +++ b/functions/src/music.js @@ -18,6 +18,7 @@ const SUNO_MODEL = "V4_5"; // const SUNO_MODEL = "V3_5"; const SUNO_CALLBACK_URL = "https://us-central1-musicland-d33f9.cloudfunctions.net/music-sunoCallback"; +// "https://music-sunocallback-ifdrkc2c3a-uc.a.run.app"; /** * Limite la longueur d'une chaîne @@ -200,7 +201,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => { "Content-Type": "application/json", Authorization: `Bearer ${SUNO_API_KEY}`, }, - } + }, ); const parsed = response.data; @@ -219,7 +220,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => { } catch (error) { console.error("❌ Erreur lors de la génération de musique:", error); throw new Error( - `Erreur lors de la génération de musique: ${error.message}` + `Erreur lors de la génération de musique: ${error.message}`, ); } }); @@ -248,7 +249,7 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => { Authorization: `Bearer ${SUNO_API_KEY}`, }, timeout: 30000, // 30 secondes de timeout - } + }, ); parsed = response.data; @@ -364,7 +365,7 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => { Authorization: `Bearer ${SUNO_API_KEY}`, }, timeout: 30000, - } + }, ); parsed = response.data; console.log("📊 Réponse timestamps Suno API:", parsed?.code, parsed?.msg); @@ -416,7 +417,7 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => { }, updatedAt: admin.firestore.FieldValue.serverTimestamp(), }, - { merge: true } + { merge: true }, ); saved = true; console.log("💾 Timestamps sauvegardés dans Firestore", { @@ -426,7 +427,7 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => { } catch (e) { console.error( "❌ Échec sauvegarde timestamps Firestore:", - e?.message || e + e?.message || e, ); } @@ -532,7 +533,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { const audioUrls = tracks .map( (t) => - t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl + t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl, ) .filter(Boolean) .slice(0, 2); @@ -547,7 +548,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { has_audio_url: !!t.audio_url, has_stream_audio_url: !!t.stream_audio_url, })), - } + }, ); } @@ -572,14 +573,14 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { }, }); const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent( - path + path, )}?alt=media&token=${token}`; console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl); return { path, url: downloadUrl }; } catch (e) { console.error( `❌ [SunoCallback] Échec save piste ${index + 1}:`, - e.message + e.message, ); return null; } @@ -599,7 +600,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { musicUrls, updatedAt: admin.firestore.FieldValue.serverTimestamp(), }, - { merge: true } + { merge: true }, ); console.log("🏷️ [SunoCallback] Projet marqué GENERATED", { projectId, diff --git a/src/navigation/MainStack.js b/src/navigation/MainStack.js index b52ad5f..a2ef955 100644 --- a/src/navigation/MainStack.js +++ b/src/navigation/MainStack.js @@ -1,18 +1,13 @@ import React from "react"; import { createStackNavigator } from "@react-navigation/stack"; - import { Palette } from "../styles"; import { Routes } from "./Routes"; - import { BottomTabScreen } from "./BottomTab"; - import Splash from "../screens/Splash"; import Onboarding from "../screens/Onboarding"; import Login from "../screens/Login"; import ForgotPassword from "../screens/ForgotPassword"; - import Notifications from "../screens/Profile/Notifications"; - import TermsOfUse from "../screens/TermsOfUse"; import PrivacyPolicy from "../screens/PrivacyPolicy"; import Writing from "../screens/Writing/Writing"; @@ -28,8 +23,6 @@ import SongReady from "../screens/Studio/SongReady"; import Regenerate from "../screens/Studio/Regenerate"; import PouchReady from "../screens/cover/PouchReady"; import PhotoCover from "../screens/cover/PhotoCover"; -import AddPhotoCover from "../screens/cover/AddPhotoCover"; -import FinishCompose from "../screens/cover/FinishCompose"; import Production from "../screens/Production/Production"; import ProductionOnboarding from "../screens/Production/ProductionOnboarding"; import DownloadSongs from "../screens/Production/DownloadSongs"; @@ -63,7 +56,8 @@ import NewMusicOptions from "../screens/NewMusicOptions"; import Register from "../screens/Register"; import CreatePassword from "../screens/CreatePassword"; import CreatePseudo from "../screens/CreatePseudo"; -import ChooseCoverType from "../screens/cover/ChooseCoverType"; +import { ChooseCoverType } from "../screens/cover/ChooseCoverType"; +import ValidateCover from "../screens/cover/ValidateCover"; const screenOptions = { headerShown: false, @@ -155,12 +149,8 @@ const screens = [ component: PhotoCover, }, { - name: Routes.AddPhotoCover, - component: AddPhotoCover, - }, - { - name: Routes.FinishCompose, - component: FinishCompose, + name: Routes.ValidateCover, + component: ValidateCover, }, { name: Routes.ProductionOnboarding, diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js index 7541dbe..c4a1995 100644 --- a/src/navigation/Routes.js +++ b/src/navigation/Routes.js @@ -38,7 +38,7 @@ export const Routes = { Regenerate: "Regenerate", PouchReady: "PouchReady", PhotoCover: "PhotoCover", - AddPhotoCover: "AddPhotoCover", + ValidateCover: "ValidateCover", FinishCompose: "FinishCompose", ProductionOnboarding: "ProductionOnboarding", diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js index ceacd1f..8a641e8 100644 --- a/src/screens/NewMusicOptions.js +++ b/src/screens/NewMusicOptions.js @@ -92,7 +92,11 @@ const NewMusicOptions = () => { } break; case 2: - navigate(Routes.ChooseCoverType); + if (selectedProject?.coverUrl) { + navigate(Routes.ValidateCover); + } else { + navigate(Routes.ChooseCoverType); + } break; case 3: console.log("test"); diff --git a/src/screens/cover/AddPhotoCover.js b/src/screens/cover/AddPhotoCover.js deleted file mode 100644 index 1320c62..0000000 --- a/src/screens/cover/AddPhotoCover.js +++ /dev/null @@ -1,123 +0,0 @@ -import { View, Text, Image } from "react-native"; -import React, { useState } from "react"; -import MusicLandHeader from "../../components/MusicLandHeader"; -import { background, icons, img } from "../../assets"; -import Page from "../../layouts/Page"; -import { goBack, navigate } from "../../navigation/NavigationService"; -import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; -import { gutters, Palette, Style } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; -import BorderGradientButton from "../../components/BorderGradientButton"; -import GradientButton from "../../components/GradientButton"; -import { launchImageLibraryAsync } from "expo-image-picker"; -import { Routes } from "../../navigation"; - -const AddPhotoCover = () => { - const [image, setImage] = useState(null); - - const onPressAddImage = async () => { - const result = await launchImageLibraryAsync({ - mediaTypes: ["images"], - allowsEditing: true, - aspect: [4, 3], - quality: 1, - }); - - if (!result.canceled) { - setImage(result.assets[0]?.uri); - } - }; - - return ( - - - - - - - - - - - - - Lust for Life - - - Lana del Rey - - - - - - - - - - - navigate(Routes.FlowSelection)} - /> - - - ); -}; - -export default AddPhotoCover; diff --git a/src/screens/cover/ChooseCoverType.js b/src/screens/cover/ChooseCoverType.js index bbd0f80..e5ea707 100644 --- a/src/screens/cover/ChooseCoverType.js +++ b/src/screens/cover/ChooseCoverType.js @@ -1,5 +1,6 @@ -import { View, StyleSheet, Image } from "react-native"; +import { View, Image } from "react-native"; import React, { useState } from "react"; +import * as ImagePicker from "expo-image-picker"; import { ai, background } from "../../assets"; import Page from "../../layouts/Page"; import { goBack, navigate } from "../../navigation/NavigationService"; @@ -9,12 +10,68 @@ import GradientButton from "../../components/GradientButton"; import { gutters } from "../../styles"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import BorderGradientButton from "../../components/BorderGradientButton"; +import { useUserData } from "../../providers/UserDataProvider"; +import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; +import useMinuit from "react-native-minuit/src/hooks/useMinuit"; -const ChooseCoverType = () => { +export const ChooseCoverType = () => { const [showIntro, setShowIntro] = useState(true); + const { selectedProject, selectedProjectId, updateProjectData } = + useUserData(); + const { setIsLoading } = useMinuit(); + + const onPickUserImage = async () => { + try { + await setIsLoading(true); + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ["images"], + allowsEditing: true, + aspect: [1, 1], + quality: 1, + }); + + const uri = result?.assets?.[0]?.uri || null; + if (!uri) return; + + const projectId = selectedProject?.id || selectedProjectId; + const path = `musics/${projectId}/userSelectedCover.png`; + + const { resultURI } = await uploadFileToFirebase({ + uri, + path, + shouldCompress: true, + fileType: "IMAGE", + }); + + if (!resultURI) { + throw new Error("Erreur lors de l'upload"); + } + await updateProjectData({ + cover: { + result: resultURI, + }, + }); + navigate(Routes.ValidateCover); + } catch (e) { + console.log("onPickUserImage error", e?.message); + } finally { + await setIsLoading(false); + } + }; + return ( - + { gap: 12, }} > - + navigate(Routes.PouchReady)} @@ -40,15 +100,3 @@ const ChooseCoverType = () => { ); }; - -export default ChooseCoverType; - -const styles = StyleSheet.create({ - img: { - width: "100%", - height: "70%", - position: "absolute", - bottom: -40, - right: -30, - }, -}); diff --git a/src/screens/cover/FinishCompose.js b/src/screens/cover/FinishCompose.js deleted file mode 100644 index b8f79c4..0000000 --- a/src/screens/cover/FinishCompose.js +++ /dev/null @@ -1,53 +0,0 @@ -import { View, Image, StyleSheet } from "react-native"; -import React from "react"; -import { ai, background } from "../../assets"; -import Page from "../../layouts/Page"; -import { goBack, navigate } from "../../navigation/NavigationService"; -import MusicLandHeader from "../../components/MusicLandHeader"; -import { gutters } from "../../styles"; -import { Routes } from "../../navigation"; -import GradientButton from "../../components/GradientButton"; -import BorderGradientButton from "../../components/BorderGradientButton"; - -const FinishCompose = () => { - return ( - - - - - - navigate(Routes.Home)} - /> - navigate(Routes.Home)} - /> - - - - ); -}; - -export default FinishCompose; - -const styles = StyleSheet.create({ - img: { - width: "100%", - height: "70%", - position: "absolute", - bottom: -40, - right: -30, - }, -}); - diff --git a/src/screens/cover/PhotoCover.js b/src/screens/cover/PhotoCover.js index d6a37fa..b5b0b6d 100644 --- a/src/screens/cover/PhotoCover.js +++ b/src/screens/cover/PhotoCover.js @@ -1,17 +1,72 @@ -import { View, Text, Image } from "react-native"; +import { ActivityIndicator, Text, View } from "react-native"; import React from "react"; import MusicLandHeader from "../../components/MusicLandHeader"; -import { background, icons, img } from "../../assets"; +import { background } from "../../assets"; import Page from "../../layouts/Page"; import { goBack, navigate } from "../../navigation/NavigationService"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import { gutters, Palette, Style } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; import BorderGradientButton from "../../components/BorderGradientButton"; import { Routes } from "../../navigation"; import GradientButton from "../../components/GradientButton"; +import * as ImagePicker from "expo-image-picker"; +import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; +import { useUserData } from "../../providers/UserDataProvider"; +import useMinuit from "react-native-minuit/src/hooks/useMinuit"; +import firebase, { tasksRef } from "../../config/firebase"; +import { Image as ExpoImage } from "expo-image"; const PhotoCover = () => { + const { selectedProject, selectedProjectId, updateProjectData } = + useUserData(); + const { setIsLoading } = useMinuit(); + const isGenerating = selectedProject?.coverStatus === "GENERATING"; + + const onChooseForegroundImage = async () => { + try { + await setIsLoading(true); + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ["images"], + allowsEditing: true, + aspect: [1, 1], + quality: 1, + }); + + const uri = result?.assets?.[0]?.uri || null; + if (!uri) return; + + const projectId = selectedProject?.id || selectedProjectId; + const path = `musics/${projectId}/userForeground.png`; + + const { resultURI } = await uploadFileToFirebase({ + uri, + path, + shouldCompress: true, + fileType: "IMAGE", + }); + + if (!resultURI) { + throw new Error("Erreur lors de l'upload"); + } + await updateProjectData({ + cover: { + ...selectedProject.cover, + userForeground: resultURI, + }, + }); + await tasksRef.add({ + type: "combine", + projectId: selectedProjectId, + status: "PENDING", + createdAt: firebase.firestore.FieldValue.serverTimestamp(), + }); + } catch (e) { + console.log("onPickUserImage error", e?.message); + } finally { + await setIsLoading(false); + } + }; + return ( @@ -19,56 +74,35 @@ const PhotoCover = () => { - - - - Lust for Life - - - Lana del Rey - - - - - + ) : ( + + {isGenerating && ( + + + + Génération en cours. + + + )} + + )} @@ -80,10 +114,13 @@ const PhotoCover = () => { gap: 12, }} > - + navigate(Routes.ValidateCover)} + title="Non, valider la pochette" + /> navigate(Routes.AddPhotoCover)} + title="Oui choisir une image" + onPress={() => onChooseForegroundImage()} /> diff --git a/src/screens/cover/PouchReady.js b/src/screens/cover/PouchReady.js index efe0598..2d6751c 100644 --- a/src/screens/cover/PouchReady.js +++ b/src/screens/cover/PouchReady.js @@ -1,6 +1,14 @@ -import { View, Text, Image, ActivityIndicator } from "react-native"; +import { + View, + Text, + Image, + ActivityIndicator, + Alert, + setTimeout, +} from "react-native"; import { Image as ExpoImage } from "expo-image"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; +import { useIsFocused } from "@react-navigation/native"; import Page from "../../layouts/Page"; import { background, icons, img } from "../../assets"; import MusicLandHeader from "../../components/MusicLandHeader"; @@ -10,35 +18,20 @@ import { gutters, Palette, Style } from "../../styles"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import { Routes } from "../../navigation"; -import { FONT_FAMILY } from "../../styles/Fonts"; import firebase, { tasksRef } from "../../config/firebase"; import { useUser } from "../../providers/UserDataProvider"; +import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; const PouchReady = () => { const { selectedProjectId, selectedProject, updateProjectData } = useUser(); - const [coverUrl, setCoverUrl] = useState(null); - const [loading, setLoading] = useState(false); - const [title, setTitle] = useState(""); - const [coverStatus, setCoverStatus] = useState(null); - const isGenerating = coverStatus === "GENERATING"; - - useEffect(() => { - const d = selectedProject || {}; - setTitle(d?.title || ""); - setCoverUrl(d?.coverUrl || null); - setCoverStatus(d?.coverStatus || null); - }, [ - selectedProject?.title, - selectedProject?.coverUrl, - selectedProject?.coverStatus, - ]); + const { setIsLoading } = useMinuit(); + const isGenerating = selectedProject?.coverStatus === "GENERATING"; + const isFocused = useIsFocused(); const generateCover = async () => { if (!selectedProjectId) return; try { - setLoading(true); - await updateProjectData({ coverStatus: "GENERATING" }); - + await setIsLoading(true); await tasksRef.add({ type: "cover", projectId: selectedProjectId, @@ -48,16 +41,44 @@ const PouchReady = () => { } catch (e) { console.log("Cover task error", e?.message); } finally { - setLoading(false); + setIsLoading(false); } }; - useEffect(() => { - if (!selectedProjectId) return; - if (!coverUrl && !isGenerating && !loading) { - generateCover(); + async function validateCover() { + try { + await setIsLoading(true); + await updateProjectData({ + cover: { + ...selectedProject.cover, + result: selectedProject.cover.generatedBackground, + }, + }); + navigate(Routes.PhotoCover); + } catch (e) { + console.log(e); + } finally { + await setIsLoading(false); } - }, [selectedProjectId, coverUrl, isGenerating]); + } + + useEffect(() => { + if ( + isFocused && + !selectedProject?.cover?.generatedBackground && + selectedProject?.coverStatus !== "GENERATING" + ) { + Alert.alert("Attention", "Générer une pochette ?", [ + { + text: "Oui", + onPress: generateCover, + }, + { + text: "Non", + }, + ]); + } + }, [selectedProject, isFocused]); return ( @@ -65,15 +86,17 @@ const PouchReady = () => { - {coverUrl ? ( + {selectedProject?.cover?.generatedBackground && !isGenerating ? ( { ...Style.containerCenter, }} > - {isGenerating || loading ? ( - - ) : ( - + {isGenerating && ( + + + + Génération en cours. + + )} )} - - - {title || "Titre"} - - - MusicLand - - - - - + {/**/} + {/* */} + {/* {title || "Titre"}*/} + {/* */} + {/* */} + {/* MusicLand*/} + {/* */} + {/**/} + {/**/} + {/* */} + {/**/} @@ -160,8 +183,8 @@ const PouchReady = () => { /> navigate(Routes.PhotoCover)} + disabled={isGenerating} + onPress={() => validateCover()} /> diff --git a/src/screens/cover/ValidateCover.js b/src/screens/cover/ValidateCover.js new file mode 100644 index 0000000..7620170 --- /dev/null +++ b/src/screens/cover/ValidateCover.js @@ -0,0 +1,139 @@ +import { View, Image } from "react-native"; +import React from "react"; +import MusicLandHeader from "../../components/MusicLandHeader"; +import { background, img } from "../../assets"; +import Page from "../../layouts/Page"; +import { goBack, navigate } from "../../navigation/NavigationService"; +import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; +import { gutters, Style } from "../../styles"; +import BorderGradientButton from "../../components/BorderGradientButton"; +import GradientButton from "../../components/GradientButton"; +import { Routes } from "../../navigation"; +import { useUserData } from "../../providers/UserDataProvider"; +import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; + +const ValidateCover = () => { + const { selectedProject, updateProjectData } = useUserData(); + + const { setIsLoading } = useMinuit(); + + async function onStartAgain() { + try { + await setIsLoading(true); + await updateProjectData({ + cover: null, + coverUrl: null, + }); + navigate(Routes.ChooseCoverType); + } catch (e) { + console.log(e); + } finally { + await setIsLoading(false); + } + } + async function onValidatePicture() { + try { + await setIsLoading(true); + await updateProjectData({ + coverUrl: selectedProject?.cover?.result, + }); + navigate(Routes.FlowSelection); + } catch (e) { + console.log(e); + } finally { + await setIsLoading(false); + } + } + return ( + + + + + + + + + + + {/**/} + {/* */} + {/* Lust for Life*/} + {/* */} + {/* */} + {/* Lana del Rey*/} + {/* */} + {/**/} + {/**/} + {/* */} + {/**/} + + + + + + + + + ); +}; + +export default ValidateCover;