diff --git a/functions/assets/musicLandProduction.png b/functions/assets/musicLandProduction.png new file mode 100644 index 0000000..3a2e05d Binary files /dev/null and b/functions/assets/musicLandProduction.png differ diff --git a/functions/helpers/prompts.js b/functions/helpers/prompts.js index 120acfe..ba7f6d0 100644 --- a/functions/helpers/prompts.js +++ b/functions/helpers/prompts.js @@ -49,7 +49,7 @@ exports.generatePicturePrompt = (project = {}) => { const prompt = ` - Créer une pochette d'album abstraite et lumineuse. + Créer une pochette lumineuse. ${title || "Sans titre"} ${tagsLine} ${lyricsLine} @@ -58,7 +58,6 @@ exports.generatePicturePrompt = (project = {}) => { Image carrée 1024x1024 pixels. - ${paletteHint} Remplis toute la surface sans laisser de bordures ou zones vides. @@ -66,7 +65,7 @@ exports.generatePicturePrompt = (project = {}) => { Texte ou typographie. Personnes ou visages. Fond blanc ou bordures. - Filigrane ou logo. + Autre logo ou filigrane que celui prévu en bas à droite. `.trim(); diff --git a/functions/package-lock.json b/functions/package-lock.json index dcc95c5..184013c 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -15,7 +15,8 @@ "firebase-admin": "^12.7.0", "firebase-functions": "^6.4.0", "fluent-ffmpeg": "^2.1.3", - "genkit": "^1.18.0" + "genkit": "^1.18.0", + "sharp": "^0.33.4" }, "devDependencies": { "eslint": "^8.15.0", @@ -62,6 +63,16 @@ "node": ">=6.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", @@ -799,6 +810,367 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -3097,7 +3469,6 @@ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", - "optional": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -3296,6 +3667,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4842,8 +5222,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.1", @@ -6205,6 +6584,58 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6311,7 +6742,6 @@ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } diff --git a/functions/package.json b/functions/package.json index 03fe9fb..3f7aebd 100644 --- a/functions/package.json +++ b/functions/package.json @@ -23,7 +23,8 @@ "firebase-admin": "^12.7.0", "firebase-functions": "^6.4.0", "fluent-ffmpeg": "^2.1.3", - "genkit": "^1.18.0" + "genkit": "^1.18.0", + "sharp": "^0.33.4" }, "devDependencies": { "eslint": "^8.15.0", diff --git a/functions/src/cover.js b/functions/src/cover.js index 516691a..44cdb65 100644 --- a/functions/src/cover.js +++ b/functions/src/cover.js @@ -3,11 +3,68 @@ const admin = require("firebase-admin"); const logger = require("firebase-functions/logger"); const { generateImageV2, - CombineCoverAndPicture, } = require("../helpers/gemini"); const { generatePicturePrompt } = require("../helpers/prompts"); +const path = require("path"); +const fs = require("fs"); +const axios = require("axios"); +const sharp = require("sharp"); const db = admin.firestore(); +const bucket = admin.storage().bucket(); +const LOGO_PATH = path.resolve( + __dirname, + "../assets/musicLandProduction.png", +); + +async function buildCoverWithLogo(backgroundUrl, targetPath) { + logger.info("🖼️ [Cover] Adding logo to generated background"); + const [{ data: backgroundBuffer }, logoBuffer] = await Promise.all([ + axios.get(backgroundUrl, { responseType: "arraybuffer" }), + fs.promises.readFile(LOGO_PATH), + ]); + + const baseImage = sharp(backgroundBuffer); + const { width = 1024, height = 1024 } = await baseImage.metadata(); + + const desiredWidth = Math.round(width * 0.32); + const margin = Math.round(width * 0.04); + + const { data: resizedLogo, info: logoInfo } = await sharp(logoBuffer) + .resize({ width: desiredWidth }) + .png() + .toBuffer({ resolveWithObject: true }); + + const left = Math.max(width - logoInfo.width - margin, 0); + const top = Math.max(height - logoInfo.height - margin, 0); + + const stampedBuffer = await baseImage + .ensureAlpha() + .composite([ + { + input: resizedLogo, + left, + top, + blend: "over", + }, + ]) + .png() + .toBuffer(); + + const token = require("crypto").randomUUID(); + await bucket.file(targetPath).save(stampedBuffer, { + resumable: false, + metadata: { + contentType: "image/png", + cacheControl: "public, max-age=31536000", + metadata: { firebaseStorageDownloadTokens: token }, + }, + }); + + return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent( + targetPath, + )}?alt=media&token=${token}`; +} // Shared core for generating and saving the cover, and updating the project async function performCoverGeneration(project) { @@ -19,12 +76,13 @@ async function performCoverGeneration(project) { promptPreview: String(prompt).slice(0, 160), }); let coverUrl = ""; + const timestamp = Date.now(); try { // Utiliser une taille fixe de 1024 pixels coverUrl = await generateImageV2( prompt, 1024, - `users/${project.userId}/projects/${project.id}/generated-${Date.now()}.png`, + `users/${project.userId}/projects/${project.id}/generated-${timestamp}.png`, ); console.log("coverUrl", coverUrl); } catch (e) { @@ -38,6 +96,19 @@ async function performCoverGeneration(project) { throw new Error("Génération d'image échouée (URL vide)"); } + let finalCoverUrl = ""; + try { + const stampedPath = `users/${project.userId}/projects/${project.id}/cover-${timestamp}-stamped.png`; + finalCoverUrl = await buildCoverWithLogo(coverUrl, stampedPath); + } catch (e) { + logger.error("❌ [Cover] Logo overlay failed", { + projectId: project.id, + error: e?.message || String(e), + }); + // En cas d'échec, on retient au moins la cover générée + finalCoverUrl = coverUrl; + } + // Mettre à jour le projet avec l'URL et le statut await db .collection("projects") @@ -46,6 +117,7 @@ async function performCoverGeneration(project) { { cover: { generatedBackground: coverUrl, + result: finalCoverUrl, }, coverStatus: "GENERATED", updatedAt: admin.firestore.FieldValue.serverTimestamp(), @@ -56,56 +128,10 @@ async function performCoverGeneration(project) { logger.info("✅ [Cover] Saved", { projectId: project.id, coverUrl, + finalCoverUrl, 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", - ); - } - - logger.info("🧩 [Combine] Inputs detected", { - bgPreview: String(background).slice(0, 80), - fgPreview: String(foreground).slice(0, 80), - }); - - const combinedUrl = await CombineCoverAndPicture( - background, - foreground, - `users/${project.userId}/projects/${project.id}/combine-${Date.now()}.png`, - ); - - // Sauvegarder le résultat et marquer comme généré - await db - .collection("projects") - .doc(project.id) - .set( - { - cover: { - ...project.cover, - result: combinedUrl, - }, - coverStatus: "GENERATED", - updatedAt: admin.firestore.FieldValue.serverTimestamp(), - }, - { merge: true }, - ); - - logger.info("✅ [Cover] Combined image saved", { - projectId: project.id, - combinedUrl, - ms: Date.now() - t0, - }); - - return combinedUrl; + return finalCoverUrl; } // Firestore trigger: création d'une tâche de génération de cover @@ -132,6 +158,21 @@ exports.onTaskCreateGenerateCover = onDocumentCreated( type, projectId, }); + + if (type === "combine") { + logger.warn("⛔ [Task] Combine task ignored (feature disabled)", { + projectId, + taskId: event?.params?.taskId, + }); + await event.data.ref.update({ + status: "CANCELLED", + error: + "La personnalisation de la pochette avec une photo n'est plus disponible.", + updatedAt: admin.firestore.FieldValue.serverTimestamp(), + }); + return; + } + await db.collection("projects").doc(projectId).update({ coverStatus: "GENERATING", updatedAt: admin.firestore.FieldValue.serverTimestamp(), @@ -154,10 +195,6 @@ exports.onTaskCreateGenerateCover = onDocumentCreated( 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 }); } await event.data.ref.update({ diff --git a/src/screens/cover/ChooseCoverType.js b/src/screens/cover/ChooseCoverType.js index 37859aa..70a7502 100644 --- a/src/screens/cover/ChooseCoverType.js +++ b/src/screens/cover/ChooseCoverType.js @@ -1,30 +1,21 @@ -import * as ImagePicker from "expo-image-picker"; import { BlurView } from "expo-blur"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { - Image, - Modal, - Pressable, - StyleSheet, - Text, - View, -} from "react-native"; +import { Image, Modal, Pressable, StyleSheet, Text, View } from "react-native"; import useMinuit from "react-native-minuit/src/hooks/useMinuit"; import { ai, background } from "../../assets"; import BorderGradientButton from "../../components/BorderGradientButton"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import GradientButton from "../../components/GradientButton"; -import MusicLandHeader from "../../components/MusicLandHeader"; import { Input } from "../../components/Input"; +import MusicLandHeader from "../../components/MusicLandHeader"; import firebase, { projectsRef, usersRef } from "../../config/firebase"; -import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; import { goBack, navigate } from "../../navigation/NavigationService"; import { useUserData } from "../../providers/UserDataProvider"; -import { buildFullName } from "../../utils/artistName"; import { gutters, Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; +import { buildFullName } from "../../utils/artistName"; export const ChooseCoverType = () => { const [showIntro, setShowIntro] = useState(true); @@ -52,11 +43,14 @@ export const ChooseCoverType = () => { return pref === "REAL_NAME" || pref === "CUSTOM"; }, [currentUserData?.artistNamePreference, currentUserData?.userName]); - const realName = useMemo(() => buildFullName(currentUserData), [ - currentUserData?.firstName, - currentUserData?.lastName, - currentUserData?.displayName, - ]); + const realName = useMemo( + () => buildFullName(currentUserData), + [ + currentUserData?.firstName, + currentUserData?.lastName, + currentUserData?.displayName, + ] + ); useEffect(() => { if (pseudoVisible) { @@ -122,48 +116,48 @@ export const ChooseCoverType = () => { [currentUID, projectId] ); - const pickUserImage = useCallback(async () => { - try { - await setIsLoading(true); - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ["images"], - allowsEditing: true, - aspect: [1, 1], - quality: 1, - }); + // const pickUserImage = useCallback(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 uri = result?.assets?.[0]?.uri || null; + // if (!uri) return; - if (!projectId) return; - const path = `musics/${projectId}/userSelectedCover.png`; + // if (!projectId) return; + // const path = `musics/${projectId}/userSelectedCover.png`; - const { resultURI } = await uploadFileToFirebase({ - uri, - path, - shouldCompress: true, - fileType: "IMAGE", - }); + // 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); - } - }, [projectId, setIsLoading, updateProjectData]); + // 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); + // } + // }, [projectId, setIsLoading, updateProjectData]); - const handlePickUserImage = useCallback(() => { - ensureArtistPreference(pickUserImage); - }, [ensureArtistPreference, pickUserImage]); + // const handlePickUserImage = useCallback(() => { + // ensureArtistPreference(pickUserImage); + // }, [ensureArtistPreference, pickUserImage]); const handleGenerateCover = useCallback(() => { ensureArtistPreference(() => navigate(Routes.PouchReady)); @@ -184,8 +178,7 @@ export const ChooseCoverType = () => { if (!realName) { setTooltip({ type: "error", - text: - "Renseigne ton prénom et nom dans ton profil ou crée un nom d'artiste.", + text: "Renseigne ton prénom et nom dans ton profil ou crée un nom d'artiste.", }); setChoiceVisible(false); setPseudoVisible(true); @@ -298,10 +291,10 @@ export const ChooseCoverType = () => { gap: 12, }} > - + /> */} { diff --git a/src/screens/cover/PhotoCover.js b/src/screens/cover/PhotoCover.js index aeb3326..a38ce45 100644 --- a/src/screens/cover/PhotoCover.js +++ b/src/screens/cover/PhotoCover.js @@ -1,15 +1,11 @@ import { Image as ExpoImage } from "expo-image"; -import * as ImagePicker from "expo-image-picker"; import React from "react"; import { ActivityIndicator, Text, View } from "react-native"; -import useMinuit from "react-native-minuit/src/hooks/useMinuit"; import { background } from "../../assets"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; -import firebase, { tasksRef } from "../../config/firebase"; import loaderMessages from "../../config/loaderMessages"; -import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; import { isWeb } from "../../hooks/useLayoutType"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; @@ -19,69 +15,30 @@ import { gutters, Palette, Style } from "../../styles"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; const PhotoCover = () => { - const { selectedProject, selectedProjectId, updateProjectData } = - useUserData(); - const { setIsLoading } = useMinuit(); + const { selectedProject } = useUserData(); const isGenerating = selectedProject?.coverStatus === "GENERATING"; const coverGenerationMessage = isWeb ? loaderMessages.photoCoverGenerationWeb : ""; - 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); - } - }; + const coverUrl = + selectedProject?.cover?.result || + selectedProject?.cover?.generatedBackground || + null; return ( - + - {selectedProject?.cover?.result && !isGenerating ? ( + {coverUrl && !isGenerating ? ( { ...Style.containerCenter, }} > - {isGenerating && ( - - - {isWeb ? ( - coverGenerationMessage ? ( + + {isGenerating ? ( + <> + + {isWeb && coverGenerationMessage ? ( { > {coverGenerationMessage} - ) : null - ) : ( - - Génération en cours. - - )} - - )} + ) : ( + + Génération en cours. + + )} + + ) : ( + + La pochette sera disponible une fois la génération + terminée. + + )} + )} @@ -147,18 +116,14 @@ const PhotoCover = () => { }} > navigate(Routes.ValidateCover)} - title={ - isGenerating ? "Génération en cours..." : "Valider la pochette" - } + onPress={() => navigate(Routes.PouchReady)} + title="Regénérer la pochette" disabled={isGenerating} /> onChooseForegroundImage()} - disabled={isGenerating} + title="Valider la pochette" + onPress={() => navigate(Routes.ValidateCover)} + disabled={isGenerating || !coverUrl} /> diff --git a/src/screens/cover/PouchReady.js b/src/screens/cover/PouchReady.js index e0d93e6..358b687 100644 --- a/src/screens/cover/PouchReady.js +++ b/src/screens/cover/PouchReady.js @@ -19,7 +19,7 @@ import { gutters, Palette, Style } from "../../styles"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; const PouchReady = () => { - const { selectedProjectId, selectedProject, updateProjectData } = useUser(); + const { selectedProjectId, selectedProject } = useUser(); const { setIsLoading } = useMinuit(); const isGenerating = selectedProject?.coverStatus === "GENERATING"; const isFocused = useIsFocused(); @@ -44,22 +44,9 @@ const PouchReady = () => { } }, [selectedProjectId, setIsLoading]); - 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); - } - } + const validateCover = () => { + navigate(Routes.ValidateCover); + }; useEffect(() => { if ( @@ -84,6 +71,11 @@ const PouchReady = () => { } }, [generateCover, isFocused, selectedProject]); + const coverPreviewUrl = + selectedProject?.cover?.result || + selectedProject?.cover?.generatedBackground || + null; + return ( @@ -98,9 +90,9 @@ const PouchReady = () => { /> - {selectedProject?.cover?.generatedBackground && !isGenerating ? ( + {coverPreviewUrl && !isGenerating ? ( { /> validateCover()} /> diff --git a/src/utils/projectStages.js b/src/utils/projectStages.js index e0a79c8..2ac3d59 100644 --- a/src/utils/projectStages.js +++ b/src/utils/projectStages.js @@ -205,7 +205,7 @@ export const getStageAction = (key, project) => { } if (!hasFinalCover) { - return { route: Routes.PhotoCover }; + return { route: Routes.ValidateCover }; } return { route: Routes.ChooseCoverType };