← back to Model Arena
gemini image-gen fallback backend for the photoshop tool (Adobe preferred when creds land); fix .env load order — design-tools registered before env was loaded, so key-gated tools never appeared
19eb62d69defaf338a957848eb60b70515754876 · 2026-07-23 09:05:38 -0700 · Steve Abrams
Files touched
M design-tools.jsM server.js
Diff
commit 19eb62d69defaf338a957848eb60b70515754876
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 09:05:38 2026 -0700
gemini image-gen fallback backend for the photoshop tool (Adobe preferred when creds land); fix .env load order — design-tools registered before env was loaded, so key-gated tools never appeared
---
design-tools.js | 33 +++++++++++++++++++++++++++++----
server.js | 18 ++++++++++--------
2 files changed, 39 insertions(+), 12 deletions(-)
diff --git a/design-tools.js b/design-tools.js
index 0e23128..13c6e7a 100644
--- a/design-tools.js
+++ b/design-tools.js
@@ -217,9 +217,33 @@ const psUsage = (id) => ({
note: 'the placeholder is swapped for the real image (data URI) when your HTML is saved — treat it as a normal <img>, style/crop with CSS (object-fit:cover etc). Do NOT invent asset ids.',
});
+// Gemini image generation — the live fallback backend when Adobe creds are absent
+const geminiKey = () => process.env.GEMINI_IMAGE_KEY || process.env.GEMINI_API_KEY_WALLCO || process.env.GEMINI_API_KEY || '';
+const geminiAvailable = () => !!geminiKey();
+async function geminiGenerate(a) {
+ const model = process.env.GEMINI_IMAGE_MODEL || 'gemini-2.5-flash-image';
+ const r = await fetch('https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent', {
+ method: 'POST', headers: { 'x-goog-api-key': geminiKey(), 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contents: [{ parts: [{ text: 'Generate an image: ' + String(a.prompt || '').slice(0, 1000) + (a.size === 'wide' ? ' (wide 16:9 landscape composition)' : a.size === 'tall' ? ' (tall 9:16 portrait composition)' : '') }] }],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ }),
+ });
+ const j = await r.json();
+ const parts = (((j.candidates || [])[0] || {}).content || {}).parts || [];
+ const img = parts.find(p => p.inlineData && p.inlineData.data);
+ if (!img) throw new Error('gemini image: ' + JSON.stringify(j.error || j.promptFeedback || 'no image in response').slice(0, 180));
+ const id = await saveAsset(Buffer.from(img.inlineData.data, 'base64'), false);
+ psLogSpend('gemini-image', 0.04);
+ return psUsage(id);
+}
+
async function fireflyGenerate(a) {
const token = await adobeToken();
- if (!token) return { unavailable: true, note: 'Adobe credentials not configured on the arena server — design with CSS gradients/SVG instead.' };
+ if (!token) {
+ if (geminiAvailable()) return geminiGenerate(a); // live fallback backend
+ return { unavailable: true, note: 'no image backend configured on the arena server — design with CSS gradients/SVG instead.' };
+ }
const sizes = { square: { width: 1024, height: 1024 }, wide: { width: 1344, height: 768 }, tall: { width: 768, height: 1344 } };
const r = await fetch('https://firefly-api.adobe.io/v3/images/generate', {
method: 'POST',
@@ -342,10 +366,11 @@ const TOOLS = [
},
];
-// photoshop joins the belt only when Adobe creds are configured — models never see a dead tool
-if (psAvailable()) TOOLS.push({
+// photoshop joins the belt when ANY image backend is configured (Adobe Firefly
+// preferred; Gemini image-gen fallback) — models never see a dead tool
+if (psAvailable() || geminiAvailable()) TOOLS.push({
name: 'photoshop',
- description: 'Adobe Photoshop/Firefly image services: generate a real photographic or artistic image from a text prompt (generate_image), or strip the background from a generated asset (remove_background). Returns an asset id + placeholder <img> you inline; the server swaps it for the actual image when your HTML is saved. Use for hero imagery a CSS gradient cannot fake. Max 2 images per artifact.',
+ description: 'Image studio: generate a real photographic or artistic image from a text prompt (generate_image)' + (psAvailable() ? ', or strip the background from a generated asset (remove_background)' : '') + '. Returns an asset id + placeholder <img> you inline; the server swaps it for the actual image when your HTML is saved. Use for hero imagery a CSS gradient cannot fake. Max 2 images per artifact.',
parameters: {
type: 'object',
properties: {
diff --git a/server.js b/server.js
index 3c42913..cb8200c 100644
--- a/server.js
+++ b/server.js
@@ -12,6 +12,16 @@ process.on('uncaughtException', (e) => console.error('[uncaughtException]', (e &
const http = require('http'), fs = require('fs'), path = require('path'), os = require('os');
const crypto = require('crypto');
const { execFile } = require('child_process');
+
+// load .env BEFORE design-tools — its tool registration (photoshop backend keys)
+// reads process.env at require time
+try {
+ for (const line of fs.readFileSync(path.join(__dirname, '.env'), 'utf8').split('\n')) {
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
+ if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
+ }
+} catch {}
+
const { TOOLS: DESIGN_TOOLS, designPack, inlineAssets, PS_DIR, PS_PUT_TOKENS } = require('./design-tools');
// headless-Chrome path (macOS default; override with CHROME_BIN)
@@ -24,14 +34,6 @@ function shootThumb(htmlPath, outPng, cb) {
'file://' + htmlPath], { timeout: 20000 }, (err) => cb && cb(err || (fs.existsSync(outPng) ? null : new Error('no png'))));
}
-// load .env (gitignored; populated from secrets-manager) without a dependency
-try {
- for (const line of fs.readFileSync(path.join(__dirname, '.env'), 'utf8').split('\n')) {
- const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
- if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
- }
-} catch {}
-
const PORT = parseInt(process.env.PORT || '9758', 10);
const BASIC_AUTH = process.env.BASIC_AUTH !== undefined ? process.env.BASIC_AUTH : 'admin:DW2024!';
const DIR = __dirname;
← ca53a7a arena: add 'continue with battles' button + challenge-level
·
back to Model Arena
·
README: note gemini fallback backend 292ca1d →