[object Object]

← 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

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 →