← back to Wallco Ai
elements: backend POST /api/admin/elements/compose — real Gemini img-to-img fuse
c812cf648f9b753afd73358d72db24e79d69bffe · 2026-05-30 21:28:10 -0700 · Steve Abrams
1-6 selected element ids -> multi-image gemini-2.5-flash-image fuse (auto mode:
1=reference, 2=blend, 3+=compose). Result inserted UNPUBLISHED (is_published=FALSE)
so it goes through the curator/settlement gate like every fresh generation;
parent_design_id linkage + appendFixEvent audit. Source pixels resolved via the
grid's /designs/img/by-id route (covers stale local_path on sparse checkouts),
local_path fast-path first. isAdmin gate, matching sibling /api/admin routes.
Pairs the already-committed frontend drawer-cta wiring (8eef445a). Smoke-tested
local :9905 — reference #54724 (10.6s) + blend #54725 (12.3s), real ~1.3MB PNGs,
both rows pub=f, parent=54723, images serve 200. LOCAL ONLY, no deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit c812cf648f9b753afd73358d72db24e79d69bffe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 30 21:28:10 2026 -0700
elements: backend POST /api/admin/elements/compose — real Gemini img-to-img fuse
1-6 selected element ids -> multi-image gemini-2.5-flash-image fuse (auto mode:
1=reference, 2=blend, 3+=compose). Result inserted UNPUBLISHED (is_published=FALSE)
so it goes through the curator/settlement gate like every fresh generation;
parent_design_id linkage + appendFixEvent audit. Source pixels resolved via the
grid's /designs/img/by-id route (covers stale local_path on sparse checkouts),
local_path fast-path first. isAdmin gate, matching sibling /api/admin routes.
Pairs the already-committed frontend drawer-cta wiring (8eef445a). Smoke-tested
local :9905 — reference #54724 (10.6s) + blend #54725 (12.3s), real ~1.3MB PNGs,
both rows pub=f, parent=54723, images serve 200. LOCAL ONLY, no deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
server.js | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 121 insertions(+)
diff --git a/server.js b/server.js
index 50463e9..cf89638 100644
--- a/server.js
+++ b/server.js
@@ -2625,6 +2625,127 @@ app.get('/api/admin/elements/items', (req, res) => {
}
});
+// ── POST /api/admin/elements/compose — REAL Gemini img-to-img fuse.
+// Body: { ids:[int], hint?:string }. 1-6 selected element designs → multi-image
+// gemini-2.5-flash-image fuse, mode auto from count (1=reference, 2=blend, 3+=compose).
+// Inserts the result UNPUBLISHED (is_published=FALSE) so it goes through the
+// curator/settlement gate like every other fresh generation. One human click =
+// one paid Gemini call. Source pixels resolved via the SAME route the grid
+// renders with (/designs/img/by-id/:id) so it works even where local_path is
+// stale on a sparse checkout.
+app.post('/api/admin/elements/compose', express.json({ limit: '16kb' }), async (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ const ids = Array.isArray(req.body && req.body.ids)
+ ? req.body.ids.map(x => parseInt(x, 10)).filter(Number.isFinite).slice(0, 6)
+ : [];
+ if (!ids.length) return res.status(400).json({ error: 'no element ids' });
+ const hint = String((req.body && req.body.hint) || '').slice(0, 600);
+ const mode = ids.length === 1 ? 'reference' : ids.length === 2 ? 'blend' : 'compose';
+
+ try {
+ const selfBase = `http://127.0.0.1:${PORT}`;
+ const imgParts = [];
+ const okIds = [];
+ let primary = null;
+ for (const id of ids) {
+ let buf = null, mime = 'image/png';
+ const d = DESIGNS.find(x => x.id === id);
+ if (d && d.local_path && fs.existsSync(d.local_path)) {
+ buf = fs.readFileSync(d.local_path);
+ } else {
+ try {
+ const resp = await fetch(`${selfBase}/designs/img/by-id/${id}`);
+ if (resp.ok) {
+ buf = Buffer.from(await resp.arrayBuffer());
+ mime = resp.headers.get('content-type') || 'image/png';
+ }
+ } catch (e) { /* skip this source */ }
+ }
+ if (!buf || !buf.length) continue;
+ imgParts.push({ inline_data: { mime_type: mime.split(';')[0].trim(), data: buf.toString('base64') } });
+ okIds.push(id);
+ if (!primary && d) primary = d;
+ }
+ if (!imgParts.length) {
+ return res.status(404).json({ error: 'no source image resolvable for the selected element(s)' });
+ }
+
+ const modePrompt = ({
+ reference: 'Use the single reference image above as inspiration. Generate a NEW, original wallcovering design in the same spirit — borrow its motif vocabulary, palette, and compositional rhythm, but produce a fresh distinct pattern, not a copy.',
+ blend: 'BLEND the two reference images above into ONE new cohesive wallcovering design. Combine the motifs of one with the palette/ground of the other (or merge both motif sets) so the result reads as a single intentional pattern, not a collage. Resolve any palette clash into one harmonious scheme.',
+ compose: `COMPOSE the ${imgParts.length} reference images above into ONE new cohesive wallcovering design. Distill the strongest motif and the most pleasing palette across all references into a single unified, balanced pattern — a deliberate new design, not a montage of the inputs.`,
+ })[mode];
+
+ const composePrompt = [
+ modePrompt,
+ hint ? `Art director note: ${hint}` : '',
+ 'Keep the flat hand-painted-silk aesthetic: hard razor-sharp silhouette edges, single-layer screenprint, NO gradient, NO atmospheric perspective, NO half-tones, NO translucency.',
+ 'Produce a clean repeating wallpaper field. No watermark, no text, no border, no signature, no reference-image framing or thumbnails — output only the final flat design.',
+ ].filter(Boolean).join('\n');
+
+ const t0 = Date.now();
+ let j;
+ try {
+ j = await geminiCall({
+ model: 'gemini-2.5-flash-image',
+ parts: [...imgParts, { text: composePrompt }],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ note: 'elements-compose-' + mode + '-from-' + okIds.join('+'),
+ });
+ } catch (e) {
+ if (e.code === 'NO_KEY') return res.status(500).json({ error: 'GEMINI_API_KEY not set' });
+ return res.status(502).json({ error: e.message });
+ }
+ const data = geminiImage(j);
+ if (!data) {
+ const text = geminiText(j);
+ return res.status(502).json({ error: 'gemini returned no image: ' + (text || '').slice(0, 240) });
+ }
+
+ const newSeed = require('crypto').randomInt(1, 2 ** 31 - 1);
+ const filename = `compose_${okIds.join('-').slice(0, 40)}_${Date.now()}_${newSeed}.png`;
+ const outPath = path.join(IMG_DIR, filename);
+ fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+
+ const esc = (v) => v == null ? 'NULL' : "'" + String(v).replace(/'/g, "''") + "'";
+ const promptText = `Elements ${mode} of ${okIds.map(i => '#' + i).join(', ')}` + (hint ? ` — ${hint}` : '') + `: ${composePrompt.slice(0, 1000)}`;
+ let newId = 0;
+ try {
+ const ins = psqlExecLocal(`
+INSERT INTO spoon_all_designs
+ (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+ image_url, local_path, dominant_hex, palette, category, is_published, parent_design_id)
+VALUES
+ (${esc((primary && primary.kind) || 'seamless_tile')}, 'wallco.ai',
+ ${(primary && primary.width_in) || 'NULL'}, ${(primary && primary.height_in) || 'NULL'}, ${(primary && primary.panels) || 'NULL'},
+ 'gemini-2.5-flash-image-compose',
+ ${esc(promptText)},
+ ${newSeed},
+ '/designs/img/by-id/__NEW__',
+ ${esc(outPath)},
+ ${esc(primary && primary.dominant_hex)},
+ ${esc(JSON.stringify((primary && primary.palette) || []))}::jsonb,
+ ${esc(primary && primary.category)},
+ FALSE,
+ ${okIds[0]})
+RETURNING id`);
+ newId = parseInt(ins, 10);
+ } catch (e) {
+ return res.status(500).json({ error: 'insert failed: ' + (e && e.message || e), saved_png: outPath });
+ }
+ if (!newId) return res.status(500).json({ error: 'insert returned no id', saved_png: outPath });
+ psqlExecLocal(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}' WHERE id=${newId}`);
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
+ try { appendFixEvent({ kind: 'elements-compose', mode, src_ids: okIds, new_id: newId, elapsed_s: elapsed }); } catch (e) {}
+ return res.json({ ok: true, new_id: newId, mode, elapsed_s: elapsed,
+ source_ids: okIds, preview_url: `/designs/img/by-id/${newId}`,
+ published: false, note: 'Generated UNPUBLISHED — review in the curator queue before it goes live.' });
+ } catch (e) {
+ console.error('[elements-compose]', e);
+ return res.status(500).json({ error: String(e && e.message || e) });
+ }
+});
+
// Human-readable failure reason from the failing lenses — "show why it failed"
// (e.g. "Mismatch at horizontal midline"). Maps the 6 lens keys to plain phrases.
function edgesFailReason(lenses, maxDe) {
← 8eef445 security: add rel=noopener noreferrer to all target=_blank l
·
back to Wallco Ai
·
cactus-curator: park gate-failed designs by default, require 98f6c51 →