forked from admin/zeropost-engine
feat: image generation через GPT-5 /v1/responses + image_generation tool
Старый endpoint /v1/images/generations на gpt-image-* возвращает temporarily unavailable уже несколько часов, а тот же ключ через /v1/responses на GPT-5 успешно генерирует картинки. - covers.js полностью переписан: generateCoverViaResponses как основной путь - tool_choice: image_generation — заставляем модель ВСЕГДА вызывать инструмент - wrappedInput: явная подсказка чтобы GPT не отвечала текстом - legacy fallback: если /responses упал — пробуем старый /v1/images/generations - sharp оптимизация: оригинал PNG → WebP 1600w q84 (уменьшение в ~30 раз) - timeout до 5 минут — GPT-5 с reasoning + image это долго
This commit is contained in:
Generated
+528
-2
@@ -15,7 +15,483 @@
|
||||
"express": "^5.2.1",
|
||||
"ioredis": "^5.11.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.21.0"
|
||||
"pg": "^8.21.0",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"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.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"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.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"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-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"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-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"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.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"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-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"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.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"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/@ioredis/commands": {
|
||||
@@ -347,7 +823,6 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1280,6 +1755,50 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@@ -1385,6 +1904,13 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@
|
||||
"express": "^5.2.1",
|
||||
"ioredis": "^5.11.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.21.0"
|
||||
"pg": "^8.21.0",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
|
||||
+118
-45
@@ -6,84 +6,154 @@ const { query } = require('../config/db');
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
||||
|
||||
// Гарантируем что директория есть
|
||||
// Опциональная оптимизация — если sharp есть, конвертим в WebP
|
||||
let sharp = null;
|
||||
try { sharp = require('sharp'); } catch {}
|
||||
|
||||
if (!fs.existsSync(UPLOADS_DIR)) {
|
||||
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует промпт для обложки на основе темы и тегов.
|
||||
* Стиль фиксированный — абстрактная геометрия в emerald-палитре.
|
||||
* Промпт для обложки в стиле сайта.
|
||||
*/
|
||||
function buildCoverPrompt({ title, tags = [] }) {
|
||||
const subject = title.replace(/[«»":?!.]/g, '').slice(0, 80);
|
||||
const subject = title.replace(/[«»":?!.]/g, '').slice(0, 100);
|
||||
const tagHint = tags.slice(0, 2).join(', ');
|
||||
return `Abstract minimalist editorial cover illustration for an article titled "${subject}".
|
||||
Style: flat geometric shapes, smooth flowing curves, isometric or layered planes.
|
||||
Color palette: emerald green (#10b981, #34d399), soft teal, warm off-white background (#fafaf9), subtle dark accents.
|
||||
Mood: clean, modern, calm, intellectual.
|
||||
Composition: balanced, plenty of negative space, no text, no letters, no people, no logos.
|
||||
${tagHint ? `Theme cues: ${tagHint}.` : ''}
|
||||
High quality vector-like editorial illustration in the style of Stripe Press, Linear, Notion blog covers.`;
|
||||
|
||||
Style: flat geometric shapes, smooth flowing curves, isometric or layered planes, vector-clean lines.
|
||||
Color palette: emerald green (#10b981, #34d399) as primary accent, soft teal, warm off-white background (#fafaf9), subtle dark accents.
|
||||
Mood: clean, modern, calm, intellectual — in the spirit of Stripe Press covers, Linear marketing illustrations, Anthropic brand visuals.
|
||||
Composition: balanced, plenty of negative space, 16:9 wide format.
|
||||
${tagHint ? `Theme cues (subtle, suggestive — not literal): ${tagHint}.` : ''}
|
||||
|
||||
Strictly: no text, no letters, no logos, no people's faces, no robots, no brains, no glowing nodes, no circuit boards.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрашивает картинку у gpt-image модели, сохраняет файл локально.
|
||||
* Возвращает публичный URL.
|
||||
* Генерирует картинку через /v1/responses + image_generation tool на GPT-5.
|
||||
* Это работает даже когда /v1/images/generations отдаёт unavailable.
|
||||
*/
|
||||
async function generateCover({ articleId, title, tags = [] }) {
|
||||
const prompt = buildCoverPrompt({ title, tags });
|
||||
const model = config.ai.models.image || 'gpt-image-1';
|
||||
async function generateCoverViaResponses({ prompt }) {
|
||||
const model = process.env.AI_MODEL_IMAGE_VIA_RESPONSES || 'gpt-5.2';
|
||||
// GPT-5 иногда не вызывает инструмент сама — даём явную инструкцию
|
||||
const wrappedInput = `Use the image_generation tool to create the following illustration. Do not write any text response, only call the tool.
|
||||
|
||||
${prompt}`;
|
||||
|
||||
let imgData;
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`${config.ai.baseUrl}/images/generations`,
|
||||
`${config.ai.baseUrl}/responses`,
|
||||
{
|
||||
model,
|
||||
prompt,
|
||||
n: 1,
|
||||
size: '1536x1024',
|
||||
input: wrappedInput,
|
||||
tools: [{ type: 'image_generation' }],
|
||||
tool_choice: { type: 'image_generation' },
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${config.ai.imageApiKey}` },
|
||||
timeout: 120000,
|
||||
timeout: 300_000, // GPT-5 reasoning + image — медленно, до 5 минут
|
||||
}
|
||||
);
|
||||
imgData = res.data.data?.[0];
|
||||
|
||||
const output = res.data?.output || [];
|
||||
const imgCall = output.find(o => o.type === 'image_generation_call');
|
||||
if (!imgCall) {
|
||||
throw new Error('No image_generation_call in response output');
|
||||
}
|
||||
if (!imgCall.result) {
|
||||
throw new Error(`image_generation_call without result, status=${imgCall.status}`);
|
||||
}
|
||||
const bytes = Buffer.from(imgCall.result, 'base64');
|
||||
return {
|
||||
bytes,
|
||||
format: imgCall.output_format || 'png',
|
||||
size: imgCall.size,
|
||||
revisedPrompt: imgCall.revised_prompt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Старый путь — на случай если шлюз внезапно починят и захотим вернуться.
|
||||
*/
|
||||
async function generateCoverViaImagesEndpoint({ prompt }) {
|
||||
const model = config.ai.models?.image || 'gpt-image-1';
|
||||
const res = await axios.post(
|
||||
`${config.ai.baseUrl}/images/generations`,
|
||||
{ model, prompt, n: 1, size: '1536x1024' },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${config.ai.imageApiKey}` },
|
||||
timeout: 120_000,
|
||||
}
|
||||
);
|
||||
const item = res.data?.data?.[0];
|
||||
if (!item) throw new Error('Empty image response');
|
||||
if (item.b64_json) return { bytes: Buffer.from(item.b64_json, 'base64'), format: 'png' };
|
||||
if (item.url) {
|
||||
const r = await axios.get(item.url, { responseType: 'arraybuffer', timeout: 60_000 });
|
||||
return { bytes: Buffer.from(r.data), format: 'png' };
|
||||
}
|
||||
throw new Error('No image data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Главный путь генерации. Использует /v1/responses, при ошибке падает в legacy.
|
||||
*/
|
||||
async function generateCover({ articleId, title, tags = [] }) {
|
||||
const prompt = buildCoverPrompt({ title, tags });
|
||||
|
||||
let img;
|
||||
let usedPath = 'responses';
|
||||
try {
|
||||
img = await generateCoverViaResponses({ prompt });
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.error?.message || err.message;
|
||||
console.warn(`[Cover] generation failed for article ${articleId}:`, msg.slice(0, 200));
|
||||
throw new Error(msg);
|
||||
console.warn(`[Cover] /responses path failed: ${msg.slice(0, 200)}`);
|
||||
// Пробуем legacy
|
||||
try {
|
||||
img = await generateCoverViaImagesEndpoint({ prompt });
|
||||
usedPath = 'images-legacy';
|
||||
} catch (err2) {
|
||||
const msg2 = err2.response?.data?.error?.message || err2.message;
|
||||
console.warn(`[Cover] legacy path failed too: ${msg2.slice(0, 200)}`);
|
||||
throw new Error(`Both image paths failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!imgData) throw new Error('Empty image response');
|
||||
// Сохраняем оригинал
|
||||
const tsKey = `${articleId}-${Date.now()}`;
|
||||
const ext = img.format || 'png';
|
||||
const originalName = `cover-${tsKey}.${ext}`;
|
||||
const originalPath = path.join(UPLOADS_DIR, originalName);
|
||||
fs.writeFileSync(originalPath, img.bytes);
|
||||
|
||||
// Получаем bytes — либо из b64, либо скачиваем по URL
|
||||
let bytes;
|
||||
if (imgData.b64_json) {
|
||||
bytes = Buffer.from(imgData.b64_json, 'base64');
|
||||
} else if (imgData.url) {
|
||||
const resp = await axios.get(imgData.url, { responseType: 'arraybuffer', timeout: 60000 });
|
||||
bytes = Buffer.from(resp.data);
|
||||
} else {
|
||||
throw new Error('No image data in response');
|
||||
// Оптимизация — если sharp есть, делаем WebP в подходящем размере
|
||||
let publicUrl = `/uploads/${originalName}`;
|
||||
let optimizedSize = null;
|
||||
if (sharp) {
|
||||
try {
|
||||
const webpName = `cover-${tsKey}.webp`;
|
||||
const webpPath = path.join(UPLOADS_DIR, webpName);
|
||||
await sharp(img.bytes)
|
||||
.resize(1600, null, { withoutEnlargement: true })
|
||||
.webp({ quality: 84 })
|
||||
.toFile(webpPath);
|
||||
const stat = fs.statSync(webpPath);
|
||||
optimizedSize = stat.size;
|
||||
publicUrl = `/uploads/${webpName}`;
|
||||
} catch (e) {
|
||||
console.warn(`[Cover] sharp optimization skipped: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const filename = `cover-${articleId}-${Date.now()}.png`;
|
||||
const filepath = path.join(UPLOADS_DIR, filename);
|
||||
fs.writeFileSync(filepath, bytes);
|
||||
|
||||
const publicUrl = `/uploads/${filename}`;
|
||||
await query(`UPDATE articles SET cover_url=$1, updated_at=NOW() WHERE id=$2`, [publicUrl, articleId]);
|
||||
|
||||
console.log(`[Cover] saved ${publicUrl} (${(bytes.length / 1024).toFixed(0)} KB)`);
|
||||
console.log(`[Cover] saved ${publicUrl} via ${usedPath} (${(img.bytes.length / 1024).toFixed(0)}KB original, ${optimizedSize ? (optimizedSize / 1024).toFixed(0) + 'KB optimized' : 'no opt'})`);
|
||||
return publicUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Дофоновая попытка сгенерировать обложки для статей без cover_url.
|
||||
* Запускается периодически — если шлюз картинок снова доступен, всё подтянется.
|
||||
*/
|
||||
async function backfillCovers({ limit = 3 } = {}) {
|
||||
const { rows } = await query(
|
||||
@@ -93,15 +163,18 @@ async function backfillCovers({ limit = 3 } = {}) {
|
||||
[limit]
|
||||
);
|
||||
let ok = 0, fail = 0;
|
||||
const results = [];
|
||||
for (const a of rows) {
|
||||
try {
|
||||
await generateCover({ articleId: a.id, title: a.title, tags: a.tags || [] });
|
||||
const url = await generateCover({ articleId: a.id, title: a.title, tags: a.tags || [] });
|
||||
ok++;
|
||||
} catch {
|
||||
results.push({ id: a.id, status: 'ok', url });
|
||||
} catch (err) {
|
||||
fail++;
|
||||
results.push({ id: a.id, status: 'fail', error: err.message.slice(0, 150) });
|
||||
}
|
||||
}
|
||||
return { processed: rows.length, ok, fail };
|
||||
return { processed: rows.length, ok, fail, results };
|
||||
}
|
||||
|
||||
module.exports = { generateCover, backfillCovers, buildCoverPrompt, UPLOADS_DIR };
|
||||
|
||||
Reference in New Issue
Block a user