[object Object]

← back to Wallco Ai

gen-luxe v2: 5 heritage-house variants + DW-texture ground anchor

77c507c6ca0b70c771c43fac607200d4211a5f4f · 2026-05-25 00:58:54 -0700 · Steve Abrams

Five luxury aesthetic variants — every one a heritage house Steve actually
carries at $100-1000/yd:
  A) de Gournay / Zuber / Gracie     — hand-painted chinoiserie silk
  B) House of Hackney                 — maximalist animal-pattern jewel-tone
  C) 1838 Wallcoverings (NEW)        — flocked-velvet damask, 2-color w/
                                        soft raised-pile edges + drop shadow
  D) Cole & Son Whimsical (NEW)      — Stag Trail / Hummingbirds engraved
                                        naturalist energy
  E) Brunschwig & Fils / Lee Jofa   — archive brick repeat (was V-B before)

Composition gate now cycles MAX=5 attempts (was 3) so every variant gets
one shot. Failure mode codified from yesterday's "way too child like" frog
regen — old prompts had Zuber/de Gournay/Brunschwig but missed the
heritage animal-pattern houses Steve named explicitly (House of Hackney,
1838 for flock effect, Cole & Son).

NEW: pickTextureBrief() pulls a real DW shopify_products row matching an
optional --texture-cat=<grasscloth|linen|silk|cork|raffia|...> and joins
shopify_color_enrichment for the dominant hex + family. The result lands
in the prompt as a concrete GROUND clause — e.g. "Brown cork (matched DW
SKU Kanda Natural Cork, dominant #D2B48C)" — so the LLM paints the motif
on top of a material we actually sell instead of hallucinating "ivory
paper". When --texture-cat is omitted, picks any natural-fiber wallcovering.

Generator tag bumped to gemini-2.5-flash-image-luxe-v2.

Also stages scripts/gen-from-stored-prompt.js from earlier in the session
(regenerates a wallco design from its STORED prompt rather than from
vision-describe of the broken PNG — the bullfrog-rescue tool that produced
#41536 from #28066's dropped-subject failure).

Files touched

Diff

commit 77c507c6ca0b70c771c43fac607200d4211a5f4f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 00:58:54 2026 -0700

    gen-luxe v2: 5 heritage-house variants + DW-texture ground anchor
    
    Five luxury aesthetic variants — every one a heritage house Steve actually
    carries at $100-1000/yd:
      A) de Gournay / Zuber / Gracie     — hand-painted chinoiserie silk
      B) House of Hackney                 — maximalist animal-pattern jewel-tone
      C) 1838 Wallcoverings (NEW)        — flocked-velvet damask, 2-color w/
                                            soft raised-pile edges + drop shadow
      D) Cole & Son Whimsical (NEW)      — Stag Trail / Hummingbirds engraved
                                            naturalist energy
      E) Brunschwig & Fils / Lee Jofa   — archive brick repeat (was V-B before)
    
    Composition gate now cycles MAX=5 attempts (was 3) so every variant gets
    one shot. Failure mode codified from yesterday's "way too child like" frog
    regen — old prompts had Zuber/de Gournay/Brunschwig but missed the
    heritage animal-pattern houses Steve named explicitly (House of Hackney,
    1838 for flock effect, Cole & Son).
    
    NEW: pickTextureBrief() pulls a real DW shopify_products row matching an
    optional --texture-cat=<grasscloth|linen|silk|cork|raffia|...> and joins
    shopify_color_enrichment for the dominant hex + family. The result lands
    in the prompt as a concrete GROUND clause — e.g. "Brown cork (matched DW
    SKU Kanda Natural Cork, dominant #D2B48C)" — so the LLM paints the motif
    on top of a material we actually sell instead of hallucinating "ivory
    paper". When --texture-cat is omitted, picks any natural-fiber wallcovering.
    
    Generator tag bumped to gemini-2.5-flash-image-luxe-v2.
    
    Also stages scripts/gen-from-stored-prompt.js from earlier in the session
    (regenerates a wallco design from its STORED prompt rather than from
    vision-describe of the broken PNG — the bullfrog-rescue tool that produced
    #41536 from #28066's dropped-subject failure).
---
 scripts/gen-from-stored-prompt.js | 137 ++++++++++++++++++++++++++
 scripts/gen-luxe.js               | 200 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 337 insertions(+)

diff --git a/scripts/gen-from-stored-prompt.js b/scripts/gen-from-stored-prompt.js
new file mode 100644
index 0000000..4b91105
--- /dev/null
+++ b/scripts/gen-from-stored-prompt.js
@@ -0,0 +1,137 @@
+#!/usr/bin/env node
+// gen-from-stored-prompt — regenerate a wallco design from its STORED
+// generation prompt (not from vision-describe of the current PNG).
+//
+// Why this exists: 28066 was prompted "drunken bullfrog with champagne
+// among bluebonnets" but SDXL/comfy produced ONLY the bluebonnets — no
+// frog. Vision-describe on the broken output sees "white flowers" and
+// loses the bullfrog intent. To actually fix it we need the ORIGINAL
+// PROMPT, distilled to a multi-instance motif description and run
+// through lib/repeat-prompt + Gemini-2.5-flash-image.
+//
+// Usage:  node scripts/gen-from-stored-prompt.js <src_id> [<motif_override>]
+
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+const { buildRepeatPrompt, inferDensity } = require('../lib/repeat-prompt');
+const { gateComposition } = require('../lib/composition-detector');
+
+const SRC_ID = parseInt(process.argv[2], 10);
+const MOTIF_OVERRIDE = process.argv[3] || null;
+if (!Number.isFinite(SRC_ID)) { console.error('usage: <src_id>'); process.exit(2); }
+
+const ROOT = path.join(__dirname, '..');
+const GEN_DIR = path.join(ROOT, 'data', 'generated');
+
+function readKey() {
+  if (process.env.GEMINI_API_KEY) return process.env.GEMINI_API_KEY;
+  const env = fs.readFileSync(path.join(ROOT, '.env'), 'utf8');
+  const m = env.match(/^GEMINI_API_KEY=(.+)$/m);
+  if (!m) throw new Error('GEMINI_API_KEY missing');
+  return m[1].trim();
+}
+
+async function geminiImage(prompt) {
+  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${readKey()}`;
+  const r = await fetch(url, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      contents: [{ parts: [{ text: prompt }] }],
+      generationConfig: { responseModalities: ['IMAGE'] },
+    }),
+  });
+  if (!r.ok) throw new Error(`gemini ${r.status}: ${(await r.text()).slice(0, 200)}`);
+  const j = await r.json();
+  const part = j?.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+  const b64 = part?.inline_data?.data || part?.inlineData?.data;
+  if (!b64) throw new Error('gemini returned no image');
+  return b64;
+}
+
+function psql(sql) {
+  const r = spawnSync('psql', ['dw_unified', '-At', '-q'], { input: sql, encoding: 'utf8' });
+  if (r.status !== 0) throw new Error(r.stderr || 'psql failed');
+  return r.stdout.trim();
+}
+
+// Distill a comfy/SDXL prompt down to ONE clean motif description that
+// describes a SINGLE instance of the subject. Strip the catalog-palette
+// boilerplate and the negative anchors.
+function distillMotif(storedPrompt) {
+  if (!storedPrompt) return null;
+  // Cut everything from the first "Professional designer-wallpaper palette"
+  // marker onward — that's the boilerplate catalog tail.
+  let s = storedPrompt.split(/Professional designer-wallpaper palette/i)[0].trim();
+  // Strip the prompt's NO-foo anti-prompts (they don't help a re-render)
+  s = s.replace(/\bNO\s+[a-z-]+(?:\s*,\s*NO\s+[a-z-]+)*/gi, '').trim();
+  s = s.replace(/\bno\s+text\s+or\s+watermark\b/gi, '').trim();
+  s = s.replace(/,\s*,/g, ',').replace(/,\s*$/, '').trim();
+  return s;
+}
+
+(async () => {
+  const row = psql(`SELECT row_to_json(t) FROM (SELECT id, category, prompt, dominant_hex, local_path FROM spoon_all_designs WHERE id=${SRC_ID}) t`);
+  if (!row) { console.error(`no row for id=${SRC_ID}`); process.exit(2); }
+  const d = JSON.parse(row);
+  console.log(`[gen-stored] src #${SRC_ID} · category=${d.category}`);
+  const motif = MOTIF_OVERRIDE || distillMotif(d.prompt);
+  if (!motif || motif.length < 20) { console.error('motif too short'); process.exit(2); }
+  console.log(`[gen-stored] motif: ${motif.slice(0, 200)}${motif.length>200?'...':''}`);
+  // Density: sparse for hero-scale subjects (a frog is a hero)
+  const density = inferDensity(motif);
+  console.log(`[gen-stored] density=${density}`);
+  const groundHex = d.dominant_hex || '#9ed0d0';
+  const groundDesc = 'tiffany robin\'s-egg blue';
+
+  let candidate = null, gate = null, attempt = 0;
+  const MAX = 3;
+  while (attempt < MAX) {
+    const prompt = buildRepeatPrompt({ motifDescription: motif, groundHex, groundDesc, density, attempt });
+    console.log(`\n[gen-stored] attempt ${attempt}`);
+    const t0 = Date.now();
+    const b64 = await geminiImage(prompt);
+    console.log(`  gen: ${((Date.now()-t0)/1000).toFixed(1)}s`);
+    gate = await gateComposition(b64, { minInstances: 3 });
+    console.log(`  composition: ${gate.ok ? 'PASS' : 'FAIL'} · n=${gate.result.instance_count} hero=${gate.result.hero_centered}`);
+    console.log(`  saw: ${gate.result.what_you_see}`);
+    if (gate.ok) { candidate = b64; break; }
+    attempt++;
+  }
+  if (!candidate) {
+    console.log(`[gen-stored] gate failed all ${MAX} attempts — accepting last anyway`);
+    candidate = await geminiImage(buildRepeatPrompt({ motifDescription: motif, groundHex, groundDesc, density, attempt: MAX-1 }));
+  }
+
+  const newSeed = require('crypto').randomInt(1, 2 ** 31 - 1);
+  const filename = `stored_${SRC_ID}_${Date.now()}_${newSeed}.png`;
+  const outPath = path.join(GEN_DIR, filename);
+  fs.writeFileSync(outPath, Buffer.from(candidate, 'base64'));
+  console.log(`\n[gen-stored] saved → ${outPath}`);
+
+  const esc = v => v == null ? 'NULL' : "'" + String(v).replace(/'/g, "''") + "'";
+  const insertSQL = `
+INSERT INTO spoon_all_designs
+  (kind, brand, width_in, height_in, generator, prompt, seed,
+   image_url, local_path, dominant_hex, category, is_published, parent_design_id)
+VALUES
+  ('seamless_tile', 'wallco.ai', 24, 24,
+   'gemini-2.5-flash-image-stored-prompt-v2',
+   ${esc('Regen of #' + SRC_ID + ' from stored prompt distilled to motif: ' + motif.slice(0, 1500))},
+   ${newSeed},
+   '/designs/img/by-id/__NEW__',
+   ${esc(outPath)},
+   ${esc(groundHex)},
+   ${esc(d.category)},
+   TRUE,
+   ${SRC_ID})
+RETURNING id;`;
+  const newId = parseInt(psql(insertSQL), 10);
+  if (!newId) throw new Error('insert failed');
+  psql(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}' WHERE id=${newId}`);
+  // Unpublish the source so the redirect picks the new fix
+  psql(`UPDATE spoon_all_designs SET is_published=FALSE WHERE id=${SRC_ID}`);
+  console.log(`[gen-stored] PG id: ${newId}`);
+  console.log(`[gen-stored] view: http://127.0.0.1:9877/design/${newId}`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/gen-luxe.js b/scripts/gen-luxe.js
new file mode 100644
index 0000000..ebef9f4
--- /dev/null
+++ b/scripts/gen-luxe.js
@@ -0,0 +1,200 @@
+#!/usr/bin/env node
+// gen-luxe — luxury-aesthetic regen for a wallco design.
+// Anchors style to the actual heritage houses Steve sells over $100/yd:
+// de Gournay + Zuber (hand-painted chinoiserie), Brunschwig & Fils + Lee Jofa
+// (heritage archive), House of Hackney (maximalist animal pattern), 1838
+// Wallcoverings (flocked velvet), Cole & Son (Whimsical Stag Trail), Mind
+// the Gap (eclectic naturalist), Gracie (museum chinoiserie), Designers
+// Guild + Scalamandré (color-confident classics), Phillip Jeffries +
+// Phillipe Romano (textile-anchored). Pulls a real DW texture from PG to
+// anchor the ground material instead of hallucinated "ivory paper".
+//
+// Explicitly NOT vinyl die-cut, NOT pop-art cartoon, NOT children's-book.
+//
+// Usage:  node scripts/gen-luxe.js <src_id> "<motif description>" [--texture-cat=<cat>]
+//
+// Examples:
+//   node scripts/gen-luxe.js 28066 "a bullfrog with champagne magnum and flute amid bluebonnets"
+//   node scripts/gen-luxe.js 41536 "a bullfrog with champagne magnum" --texture-cat=linen
+
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+const { gateComposition } = require('../lib/composition-detector');
+
+const SRC_ID = parseInt(process.argv[2], 10);
+const MOTIF = process.argv[3];
+const TEXTURE_CAT = (process.argv.find(a => a.startsWith('--texture-cat=')) || '').split('=')[1] || '';
+if (!Number.isFinite(SRC_ID) || !MOTIF) {
+  console.error('usage: <src_id> "<motif>" [--texture-cat=<grasscloth|linen|cork|silk|mica|...>]');
+  process.exit(2);
+}
+
+const ROOT = path.join(__dirname, '..');
+const GEN_DIR = path.join(ROOT, 'data', 'generated');
+
+function readKey() {
+  if (process.env.GEMINI_API_KEY) return process.env.GEMINI_API_KEY;
+  const env = fs.readFileSync(path.join(ROOT, '.env'), 'utf8');
+  const m = env.match(/^GEMINI_API_KEY=(.+)$/m);
+  if (!m) throw new Error('GEMINI_API_KEY missing');
+  return m[1].trim();
+}
+function psql(sql) {
+  const r = spawnSync('psql', ['dw_unified', '-At', '-q'], { input: sql, encoding: 'utf8' });
+  if (r.status !== 0) throw new Error(r.stderr || 'psql failed');
+  return r.stdout.trim();
+}
+async function geminiImage(prompt) {
+  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${readKey()}`;
+  const r = await fetch(url, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      contents: [{ parts: [{ text: prompt }] }],
+      generationConfig: { responseModalities: ['IMAGE'] },
+    }),
+  });
+  if (!r.ok) throw new Error(`gemini ${r.status}: ${(await r.text()).slice(0, 200)}`);
+  const j = await r.json();
+  const part = j?.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+  const b64 = part?.inline_data?.data || part?.inlineData?.data;
+  if (!b64) throw new Error('gemini returned no image');
+  return b64;
+}
+
+// Five variants — every one a heritage house Steve actually sells.
+// Order is intentional: A (de Gournay hand-painted), B (House of Hackney
+// maximalist), C (1838 flocked velvet), D (Cole & Son Whimsical), E
+// (Brunschwig & Fils archive). Composition-detector cycles until one
+// passes the multi-instance gate.
+//
+// Each variant takes an optional `textureBrief` describing the actual DW
+// ground material the design will sit on (pulled live from
+// shopify_products via PG, then summarized: "soft moss-green sisal" etc).
+// That goes into the GROUND clause so the LLM paints the motif on a
+// material the prompt explicitly names rather than hallucinating.
+function buildLuxePrompt(motif, attempt, textureBrief) {
+  const ground = textureBrief
+    ? `Ground: ${textureBrief} — paint the motif on top of this actual DW textile ground, with the textile's slubs and weave subtly visible behind the motif.`
+    : `Ground: hand-painted linen or grasscloth ground, muted natural pigment with subtle weave texture visible.`;
+  const variants = [
+    // A — de Gournay / Zuber / Gracie hand-painted chinoiserie
+    `A luxury hand-painted wallcovering in the tradition of de Gournay, Zuber, and Gracie — the $400-$1000/yd gold standard of hand-painted chinoiserie silk panels. The motif: ${motif}, rendered as a delicate hand-painted naturalist illustration with confident gouache brushwork. ${ground} Multiple instances of the same motif arranged as a diamond half-drop repeat (each row shifted half a column), 6 to 9 instances per 24" tile, generous breathing room between them. Aesthetic: museum-grade botanical archive, Brunschwig & Fils heritage, Anna French naturalist watercolor — refined, restrained, archival. Limited 3-4 color palette pulled from real heritage wallpaper: muted tiffany blue, ivory cream, deep sage, soft champagne. Single flat hand-painted color per shape with confident edges — NOT vinyl die-cut, NOT pop-art cartoon, NOT children's-book, NOT vector. Painterly imperfections allowed. Seamless 24" tile: motifs clipped on the right continue on the left of the SAME tile. NO centered hero motif. NO enlarged feature instance.`,
+
+    // B — House of Hackney maximalist animal-pattern
+    `Maximalist designer wallcovering in the unmistakable House of Hackney aesthetic — saturated, painterly, the motif rendered with naturalist authority but jewel-toned drama (think their "Limerence", "Hackney Empire", "Artemis" patterns). The motif: ${motif}. ${ground} Layout: drop-half repeat, 6-9 instances per 24" tile, each motif painted with confident brushwork and rich tonal modeling. Palette: 4-5 saturated heritage pigments — emerald, oxblood, antique gold, deep teal, ivory. The motif is dignified, never cute — eyes are realistic, no rosy cheeks, no anthropomorphic exaggeration. Painted on a richly-textured ground (silk satin or linen), with subtle ground variation. NOT cartoon, NOT pop-art, NOT children's-book, NOT vector illustration. Seamless 24" wrap, with motifs spanning tile edges. Reference: House of Hackney's own animal-pattern collections at $300-500/yd.`,
+
+    // C — 1838 Wallcoverings flocked velvet effect
+    `Heritage flocked-velvet wallcovering in the 1838 Wallcoverings tradition — the velvet-flock-on-silk technique that gives $400/yd damasks their dimensional texture. The motif: ${motif}, rendered as a flat velvet-flock silhouette in a single deep heritage color, with a subtle raised-pile soft edge as if the motif is physically flocked onto the ground. ${ground} Layout: diamond half-drop repeat, 6-9 instances per 24" tile, evenly spaced. Color: 2 colors only — one deep velvet flock color (emerald, oxblood, deep teal, midnight, or aubergine) plus one ground color (ivory silk, champagne, or pale gold). The motif must read as a SHADOWED FLOCKED VELVET DAMASK — NOT a flat printed sticker, NOT outlined comic, NOT children's-book illustration. Subtle drop-shadow under each motif suggesting the velvet pile catches the light. Seamless 24" tile, motifs wrap edges. Reference: 1838's Hesketh, Avington, and Capri flocked velvet collections.`,
+
+    // D — Cole & Son Whimsical (Stag Trail / Hummingbirds energy)
+    `Designer wallcovering in the Cole & Son Whimsical tradition — the same engraved-naturalist energy as their Hummingbirds, Stag Trail, and Versailles Grand patterns ($150-250/yd). The motif: ${motif}, rendered with the precision of a 19th-century engraved naturalist plate — clean confident linework, controlled shading via crosshatch or stipple, NOT cartoon shading. ${ground} Layout: a refined brick offset repeat, 6-9 instances per 24" tile, each motif scaled small enough to read as an all-over pattern (not a hero). Palette: 3 colors — a soft heritage ground (pale celadon, antique ivory, dusty sage), one mid-tone motif color (charcoal, deep teal, oxblood), and one accent. The motif is rendered with naturalist dignity — no exaggerated cuteness, no oversized eyes, no anthropomorphic cartoon energy. Think Audubon plate, not children's book. Seamless 24" tile, motifs wrap edges.`,
+
+    // E — Brunschwig & Fils / Lee Jofa archive (brick repeat)
+    `Heritage botanical wallcovering. Brunschwig & Fils + Lee Jofa archive style — the kind of $350/yd designer wallpaper sold in to-the-trade showrooms. The motif (repeated 6-9 times across a 24" tile in a refined brick offset repeat): ${motif}. ${ground} Hand-painted naturalist rendering with subtle tonal variation within each motif — gouache-on-linen aesthetic. Muted heritage palette: dusty teal, antique cream, restrained sage, vintage champagne. Each motif sits with quiet authority — no exaggerated cartoonish features, no oversized eyes, no rosy cheeks, no anthropomorphic cuteness. The subject is rendered as a serious naturalist or chinoiserie subject — Audubon plate, Zuber panel detail. Single-layer flat color per shape with confident painterly edges. Seamless 24" tile wrap left/right and top/bottom.`,
+  ];
+  return variants[attempt % variants.length];
+}
+
+// Pull one ground texture from the unified /api/bg-textures pool (or
+// directly from PG if the server isn't reachable) — returns a short
+// natural-language brief the LLM can paint over.
+function pickTextureBrief(category) {
+  const cat = (category || '').toLowerCase();
+  const where = cat
+    ? `AND p.title ~* '\\m(${cat})\\M'`
+    : `AND p.title ~* '\\m(grasscloth|linen|silk|cork|raffia|madagascar|sisal)\\M'`;
+  const sql = `
+    SELECT
+      regexp_replace(split_part(p.title, '|', 1), '\\s+(Wallpaper|Wallcoverings?|Vinyl|-\\s*Sample)\\s*$', '', 'gi') AS name,
+      c.dominant_hex,
+      c.color_family,
+      CASE
+        WHEN p.title ~* '\\mgrasscloth\\M' THEN 'grasscloth'
+        WHEN p.title ~* '\\mlinen\\M'      THEN 'linen'
+        WHEN p.title ~* '\\msilk\\M'       THEN 'silk'
+        WHEN p.title ~* '\\mcork\\M'       THEN 'cork'
+        WHEN p.title ~* '\\mraffia\\M'     THEN 'raffia'
+        WHEN p.title ~* '\\mmadagascar\\M' THEN 'madagascar'
+        WHEN p.title ~* '\\msisal\\M'      THEN 'sisal'
+        ELSE 'natural textile'
+      END AS material
+    FROM shopify_products p
+    JOIN shopify_color_enrichment c ON c.shopify_id = p.shopify_id
+    WHERE p.product_type ILIKE '%wallcovering%'
+      AND p.image_url IS NOT NULL
+      AND c.dominant_hex IS NOT NULL
+      ${where}
+    ORDER BY random() LIMIT 1`;
+  try {
+    const out = psql(sql);
+    if (!out) return null;
+    const [name, hex, family, mat] = out.split('|');
+    if (!mat || !hex) return null;
+    return `${family || 'muted'} ${mat} (matched DW SKU ${name}, dominant ${hex})`;
+  } catch (e) {
+    console.warn('[gen-luxe] texture pick failed:', e.message);
+    return null;
+  }
+}
+
+(async () => {
+  const row = psql(`SELECT row_to_json(t) FROM (SELECT id, category, dominant_hex FROM spoon_all_designs WHERE id=${SRC_ID}) t`);
+  if (!row) { console.error(`no row for id=${SRC_ID}`); process.exit(2); }
+  const d = JSON.parse(row);
+  console.log(`[gen-luxe] src #${SRC_ID} · category=${d.category}`);
+  console.log(`[gen-luxe] motif: ${MOTIF}`);
+
+  const textureBrief = pickTextureBrief(TEXTURE_CAT);
+  if (textureBrief) console.log(`[gen-luxe] ground texture: ${textureBrief}`);
+  else              console.log(`[gen-luxe] ground texture: generic (PG pick returned nothing)`);
+
+  let candidate = null, gate = null, attempt = 0;
+  const MAX = 5;
+  while (attempt < MAX) {
+    const prompt = buildLuxePrompt(MOTIF, attempt, textureBrief);
+    console.log(`\n[gen-luxe] attempt ${attempt} (variant ${'ABCDE'[attempt%5]})`);
+    const t0 = Date.now();
+    const b64 = await geminiImage(prompt);
+    console.log(`  gen: ${((Date.now()-t0)/1000).toFixed(1)}s`);
+    gate = await gateComposition(b64, { minInstances: 3 });
+    console.log(`  composition: ${gate.ok ? 'PASS' : 'FAIL'} · n=${gate.result.instance_count} hero=${gate.result.hero_centered}`);
+    console.log(`  saw: ${gate.result.what_you_see}`);
+    if (gate.ok) { candidate = b64; break; }
+    attempt++;
+  }
+  if (!candidate) {
+    console.log(`[gen-luxe] gate failed all ${MAX}, accepting last`);
+    candidate = await geminiImage(buildLuxePrompt(MOTIF, MAX-1, textureBrief));
+  }
+
+  const newSeed = require('crypto').randomInt(1, 2 ** 31 - 1);
+  const filename = `luxe_${SRC_ID}_${Date.now()}_${newSeed}.png`;
+  const outPath = path.join(GEN_DIR, filename);
+  fs.writeFileSync(outPath, Buffer.from(candidate, 'base64'));
+  console.log(`\n[gen-luxe] saved → ${outPath}`);
+  const esc = v => v == null ? 'NULL' : "'" + String(v).replace(/'/g, "''") + "'";
+  const insertSQL = `
+INSERT INTO spoon_all_designs
+  (kind, brand, width_in, height_in, generator, prompt, seed,
+   image_url, local_path, dominant_hex, category, is_published, parent_design_id)
+VALUES
+  ('seamless_tile', 'wallco.ai', 24, 24,
+   'gemini-2.5-flash-image-luxe-v2',
+   ${esc('Luxe regen of #' + SRC_ID + ' anchored to de Gournay / House of Hackney / 1838-flock / Cole & Son / Brunschwig & Fils.' + (textureBrief ? ' Ground: ' + textureBrief + '.' : '') + ' Motif: ' + MOTIF)},
+   ${newSeed},
+   '/designs/img/by-id/__NEW__',
+   ${esc(outPath)},
+   ${esc(d.dominant_hex)},
+   ${esc(d.category)},
+   TRUE,
+   ${SRC_ID})
+RETURNING id;`;
+  const newId = parseInt(psql(insertSQL), 10);
+  if (!newId) throw new Error('insert failed');
+  psql(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}' WHERE id=${newId}`);
+  psql(`UPDATE spoon_all_designs SET is_published=FALSE WHERE id=${SRC_ID}`);
+  console.log(`[gen-luxe] PG id: ${newId}`);
+  console.log(`[gen-luxe] view: http://127.0.0.1:9877/design/${newId}`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });

← c6011e8 design page admin sheet: add ⎋ Logout button next to close  ·  back to Wallco Ai  ·  fix(edges-scan): move scanner from ~/.claude/skills into pro 5d5b7c0 →