← back to Wallco Ai
feat(scenic+ux): 6 Gracie-language colorways + 3 reviewer-redo murals + tiles-readout + sub-brand proposal
5782b5100bdd087959cac3538c4d4fdcb70f0d81 · 2026-05-21 19:28:47 -0700 · Steve Abrams
Three batches landed this turn, all from the 2026-05-21 reviewer panel
recommendations (interior-designer + ui-ux-designer + debate-team-fast):
Designs (Gemini image-edit from #39055):
- 39101-39106 — 6 colorway variants of the burnt-umber Pass-3 winner
(celadon, oxblood, ivory-on-black, indigo, sand-on-bone, eucalyptus).
Preserves #39055's flat hand-painted-silk Zuber/Gracie/de Gournay
silhouette composition; only recolors. The interior-designer panel:
"the brand's defensible product is this aesthetic; multiply it."
- 39107 — desert-coral redo of #39062 (the weakest of the original 6,
uncanny photoreal-flat hybrid). Re-rendered from #39055 with coral as
the single ink. Old #39062 unpublished.
- 39108 — B&W agave (#38848) recomposed off-center with a second sepia
ink tone. Centered-radial was the cardinal mural sin per the reviewer.
Old #38848 unpublished.
- 39109 — saguaro dusk (#38908) with cobalt-orange dusk gradient
swapped for a single oyster-pink wash. Reviewer panel: "stock postcard
palette → curated quiet twilight." Old #38908 unpublished.
UI (server.js):
- New #tiles-readout block above the room-setting #scale-viewport:
"Your 10 × 9 ft wall will hold 5 tiles wide × 9 repeats tall
(5 drops at 24" roll · 24" repeat)". Hidden in mural mode; live-updates
from readWallSpec() on every wall/roll/repeat change. Fixes the
ui-ux-designer panel finding that at default zoom the viewport shows
a magnified excerpt, not the full wall — customer can't visually count
tiles.
Sub-brand proposal:
- docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md drafts the wallco/Scenic
(90 photoreal hospitality murals) + wallco/Studio (7+ Gracie register)
split with names TBD, trade-pricing question open, PG migration plan
spec'd. Memory entry in ~/.claude flags it as awaiting Steve approval
before any catalog rename ships.
Scripts:
- scripts/gemini-39055-colorways.js — single-source 6-colorway renderer
- scripts/gemini-rerender-weak3.js — targeted weakest-of-batch redos
Memory patches: feedback_wallco_product_page_mode_aware.md gained 5 new
"Known holes" called out by the ui-ux-designer panel review (scale-readout
regex stale-read, dropsPerRoll silent fallback on tall ceilings, count vs
data-level ambiguity, savedWidth localStorage vs active-tab stale-read,
and the missing tiles-readout — now shipped).
Cost: 9 × ~$0.04 ≈ $0.36 on Gemini image-edit. Wall ~90s total.
Files touched
A docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.mdA scripts/gemini-39055-colorways.jsA scripts/gemini-rerender-weak3.jsM server.js
Diff
commit 5782b5100bdd087959cac3538c4d4fdcb70f0d81
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 21 19:28:47 2026 -0700
feat(scenic+ux): 6 Gracie-language colorways + 3 reviewer-redo murals + tiles-readout + sub-brand proposal
Three batches landed this turn, all from the 2026-05-21 reviewer panel
recommendations (interior-designer + ui-ux-designer + debate-team-fast):
Designs (Gemini image-edit from #39055):
- 39101-39106 — 6 colorway variants of the burnt-umber Pass-3 winner
(celadon, oxblood, ivory-on-black, indigo, sand-on-bone, eucalyptus).
Preserves #39055's flat hand-painted-silk Zuber/Gracie/de Gournay
silhouette composition; only recolors. The interior-designer panel:
"the brand's defensible product is this aesthetic; multiply it."
- 39107 — desert-coral redo of #39062 (the weakest of the original 6,
uncanny photoreal-flat hybrid). Re-rendered from #39055 with coral as
the single ink. Old #39062 unpublished.
- 39108 — B&W agave (#38848) recomposed off-center with a second sepia
ink tone. Centered-radial was the cardinal mural sin per the reviewer.
Old #38848 unpublished.
- 39109 — saguaro dusk (#38908) with cobalt-orange dusk gradient
swapped for a single oyster-pink wash. Reviewer panel: "stock postcard
palette → curated quiet twilight." Old #38908 unpublished.
UI (server.js):
- New #tiles-readout block above the room-setting #scale-viewport:
"Your 10 × 9 ft wall will hold 5 tiles wide × 9 repeats tall
(5 drops at 24" roll · 24" repeat)". Hidden in mural mode; live-updates
from readWallSpec() on every wall/roll/repeat change. Fixes the
ui-ux-designer panel finding that at default zoom the viewport shows
a magnified excerpt, not the full wall — customer can't visually count
tiles.
Sub-brand proposal:
- docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md drafts the wallco/Scenic
(90 photoreal hospitality murals) + wallco/Studio (7+ Gracie register)
split with names TBD, trade-pricing question open, PG migration plan
spec'd. Memory entry in ~/.claude flags it as awaiting Steve approval
before any catalog rename ships.
Scripts:
- scripts/gemini-39055-colorways.js — single-source 6-colorway renderer
- scripts/gemini-rerender-weak3.js — targeted weakest-of-batch redos
Memory patches: feedback_wallco_product_page_mode_aware.md gained 5 new
"Known holes" called out by the ui-ux-designer panel review (scale-readout
regex stale-read, dropsPerRoll silent fallback on tall ceilings, count vs
data-level ambiguity, savedWidth localStorage vs active-tab stale-read,
and the missing tiles-readout — now shipped).
Cost: 9 × ~$0.04 ≈ $0.36 on Gemini image-edit. Wall ~90s total.
---
docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md | 119 ++++++++++++++++++++
scripts/gemini-39055-colorways.js | 140 ++++++++++++++++++++++++
scripts/gemini-rerender-weak3.js | 163 ++++++++++++++++++++++++++++
server.js | 30 +++++
4 files changed, 452 insertions(+)
diff --git a/docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md b/docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md
new file mode 100644
index 0000000..254b05e
--- /dev/null
+++ b/docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md
@@ -0,0 +1,119 @@
+# wallco.ai — sub-brand proposal: Scenic + Studio
+
+Drafted 2026-05-21 by the panel review (interior-designer + ui-ux-designer + debate-team-fast).
+Status: **proposal — awaiting Steve approval before any catalog rename.**
+
+---
+
+## The problem
+
+The wallco.ai catalog now has **97 mural-class designs** in three internal categories:
+
+| Category | Count | Aesthetic | Generated by |
+|---|---|---|---|
+| `cactus-11ft-mural` | 58 | Photoreal Sonoran landscape | SDXL alone |
+| `tree-mural` | 32 | Photoreal scenic landscape | SDXL alone |
+| `cactus-pine-scenic` | 7 (Pass-3 + redo) | Flat hand-painted-silk (Gracie/de Gournay register) | SDXL → Gemini image-edit |
+
+The original wallco.ai pitch was "AI-generated luxury wallcovering" with an implicit aesthetic anchor on **flat hand-painted scenic register** (Zuber / Gracie / de Gournay). The Pass-3 batch nailed that aesthetic; the other 90 designs landed as **photoreal landscape murals** that LOOK luxurious but are functionally a different product class.
+
+The interior-designer panel was clear: **photoreal at 11ft is a commodity** (Phillip Jeffries, Calico, Rebel Walls all do it better). **Flat hand-painted at sub-$1,500/panel is a moat** (nobody under Gracie/de Gournay is shipping it).
+
+Three reads of the situation, per the debate-team:
+- Catalog diversity (different aesthetics for different rooms)
+- Brand confusion (undermines the hand-painted pitch)
+- Stealth strategy shift (photoreal is where the model is strongest)
+
+Conclusion: **all three reads are simultaneously true.** The honest fix is to NAME the two product lines so customers and trade designers can tell them apart.
+
+---
+
+## The proposal
+
+### wallco / **Scenic**
+
+The photoreal hospitality mural line.
+
+- **Subject:** Photoreal landscape — cactus, redwood, joshua tree, cypress, oak, eucalyptus, agave
+- **Aesthetic:** Photoreal SDXL, atmospheric depth, sunset gradients allowed
+- **Form factor:**
+ - 7.5 ft × 11 ft single-panel (Sonoran single-subject hero) — *was `cactus-11ft-mural`*
+ - 12 ft × 11 ft 4-panel full-room scenic — *was `tree-mural`*
+- **Target install:** Boutique hospitality lobbies, hotel dining rooms, corridor accent walls, residential dining/family-room accents
+- **Buyer:** Interior designer specifying a "view-through-a-window" trompe-l'oeil mural; hospitality procurement
+- **Price tier:** $12-22/sq-ft installed list ($150-250/panel residential)
+- **Competitive frame:** Phillip Jeffries Indigo, Calico Wallpaper "Wonderland," Rebel Walls — but at a fraction of the price
+- **Count today:** 90 designs
+- **Generator pipeline:** SDXL only
+
+### wallco / **Studio**
+
+The flat hand-painted scenic line. *The moat.*
+
+- **Subject:** Flat silhouette scenic — cactus, Monterey pine, agave, ocotillo, mountains, sky
+- **Aesthetic:** Single-layer hand-blocked silkscreen, 2-tone flat register, hard razor-sharp silhouette edges, no gradient, no atmospheric perspective
+- **Form factor:**
+ - 60 ft × 11 ft wraparound (20 panels × 36" × 132") — *was `cactus-pine-scenic`*
+ - 12 ft × 11 ft 4-panel single-wall scenic *(future, when we render some)*
+ - 11 ft single-panel accent *(future)*
+- **Target install:** Luxury hospitality (Four Seasons, Auberge, Aman), high-end residential dining rooms, corporate boardrooms, design-forward retail
+- **Buyer:** Senior interior designer; Designers Today reader; specifier on a $1M+ project
+- **Price tier:** $40-80/sq-ft installed list ($800-1,200/panel)
+- **Competitive frame:** Zuber & Cie, Gracie, de Gournay — at 60-70% of their price point
+- **Count today:** 7 designs (the Pass-3 cactus-pine-scenic batch + the 6 colorway variants being rendered right now)
+- **Generator pipeline:** SDXL → `gemini-2.5-flash-image-edit` 2-pass (the recipe in `feedback_wallco_gemini_edit_recovers_sdxl_aesthetic.md`)
+
+---
+
+## What needs to change
+
+### 1. PG categories
+Rename `category` column values:
+- `cactus-11ft-mural` → `scenic-cactus-11ft`
+- `tree-mural` → `scenic-tree-fullroom`
+- `cactus-pine-scenic` → `studio-cactus-pine-scenic`
+
+OR add a new `product_line` column to `spoon_all_designs` populated as `Scenic` | `Studio` | NULL. Less invasive — old category strings stay valid for any external references.
+
+### 2. Catalog URL structure
+- `/scenic` → photoreal hospitality landing page
+- `/studio` → flat hand-painted hospitality landing page
+- `/designs` → unified browse, both lines, with a sub-brand chip filter
+- Per-design URL stays `/design/:id` (already canonical); add a `data-product-line` attribute the rendered HTML so the page chrome can theme accordingly
+
+### 3. Page copy
+Lead headline on `/scenic`: *"Photoreal hospitality murals — Phillip Jeffries quality, AI-scale catalog, sub-$250 per panel."*
+Lead headline on `/studio`: *"Hand-painted-silk scenic wallcovering in the Gracie / de Gournay register — at 60% of their price point."*
+
+### 4. Trade gates
+Studio products gate pricing behind "Contact for trade pricing" (the Gracie/de Gournay convention). Scenic products show the public price.
+
+### 5. Image-edit pipeline as default for Studio
+Per the debate-team consensus on Q1 — `--two-pass` flag, mandatory for Studio batches, opt-out only with explicit reason. Cost ($0.04/design) is priced into the Studio tier.
+
+---
+
+## What does NOT need to change
+
+- Existing customer-facing URLs (`/design/:id`) stay live
+- Existing 97 designs stay published — no deletion
+- The mural-stepper + room-setting preview stay as-is (already mode-aware)
+- The SDXL Pass-1 + Pass-2 history rows stay in DB as unpublished — internal record of how we got here
+
+---
+
+## Open questions for Steve
+
+1. Are the names right? Steve has veto on "Scenic" and "Studio" — alternatives include:
+ - **wallco / Hospitality** + **wallco / Atelier**
+ - **wallco / Vista** + **wallco / Plaster**
+ - **wallco / Wide** + **wallco / Flat**
+2. Trade-pricing gate on Studio — yes / no?
+3. Should the existing 90 Scenic designs get a "Hospitality" sub-tag (Aman / Auberge / Four Seasons style packs), or is the single category enough?
+4. Next-batch target: 12 more Studio designs at #39055 quality level? Or scale Scenic first?
+
+---
+
+## Memory entry summary
+
+This proposal is paired with a memory entry at `~/.claude/projects/-Users-stevestudio2/memory/project_wallco_scenic_studio_subbrand.md` (creating now) that records the proposal as the canonical brand structure pending approval. Once Steve approves the names + the rename plan, that memory becomes the rule and the catalog UI follows.
diff --git a/scripts/gemini-39055-colorways.js b/scripts/gemini-39055-colorways.js
new file mode 100644
index 0000000..0de7185
--- /dev/null
+++ b/scripts/gemini-39055-colorways.js
@@ -0,0 +1,140 @@
+#!/usr/bin/env node
+/**
+ * 6 colorway variants of the #39055 burnt-umber Gracie-language cactus-pine
+ * scenic. Source = the Pass-3 winning aesthetic; output = same composition,
+ * same flat hand-painted silhouette, NEW ink+ground palettes.
+ *
+ * Per the interior-designer panel review (2026-05-21): "#39055 is the
+ * single most spec-able image of the six; the brand should multiply this
+ * exact aesthetic across 6 colorways."
+ *
+ * Cost: 6 × ~$0.04 = ~$0.24. Wall: ~1 min.
+ */
+'use strict';
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { execSync, spawnSync } = require('child_process');
+
+const KEY = process.env.GEMINI_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
+const SRC_ID = 39055;
+
+const DB = 'dw_unified';
+function psql(sql) { return execSync(`psql ${DB} -At -q`, { input: sql, encoding: 'utf8' }).trim(); }
+function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
+
+const COLORWAYS = [
+ { slug: 'celadon', ground: '#A8B8A8', ground_name: 'pale celadon green-grey jade', figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+ { slug: 'oxblood', ground: '#6B2A22', ground_name: 'deep oxblood cochineal red', figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+ { slug: 'ivory-on-black', ground: '#1A1A1A', ground_name: 'matte charcoal-black', figure: '#F0E8D8', figure_name: 'warm ivory cream' },
+ { slug: 'indigo', ground: '#1E3550', ground_name: 'natural indigo-dye deep blue', figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+ { slug: 'sand-on-bone', ground: '#E8DCC4', ground_name: 'warm chalky bone-cream paper', figure: '#A8895C', figure_name: 'soft sand-taupe ink' },
+ { slug: 'eucalyptus', ground: '#7A8B7E', ground_name: 'sage-eucalyptus green-grey', figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+];
+
+(async function main() {
+ const row = psql(`SELECT id, local_path, prompt, seed, width_in, height_in, panels, dominant_hex, palette, motifs, tags
+ FROM spoon_all_designs WHERE id = ${SRC_ID}`);
+ if (!row) { console.error(`source #${SRC_ID} not found`); process.exit(1); }
+ const [_id, srcPath, _srcPrompt, _seed, w, h, panels, _dom, _pal, motifs, tags] = row.split('|');
+ if (!fs.existsSync(srcPath)) { console.error(`source PNG missing: ${srcPath}`); process.exit(1); }
+
+ const imageB64 = fs.readFileSync(srcPath).toString('base64');
+ const newIds = [];
+
+ console.log(`6 Gracie-language colorways via Gemini image-edit from #${SRC_ID}\n`);
+
+ for (let i = 0; i < COLORWAYS.length; i++) {
+ const cw = COLORWAYS[i];
+ const editPrompt = (
+ `Recolor this hand-painted cactus and Monterey pine scenic mural to a NEW colorway, ` +
+ `preserving the EXACT composition, silhouettes, layout, and flat hand-painted-silk register. ` +
+ `Do NOT change positions of saguaros, pines, mountains, or ground line. Do NOT re-introduce ` +
+ `photoreal shading, gradients, or atmospheric perspective — keep the flat 2-tone Zuber/Gracie/ ` +
+ `de Gournay aesthetic intact. ` +
+ `\n\nNew palette: ` +
+ `Ground (sky, mountain band, ground plane all share the same flat ground tone): ${cw.ground_name} (hex ${cw.ground}). ` +
+ `Figure (all silhouettes — cactus, pine trunks, pine canopies, mountain edges): ${cw.figure_name} (hex ${cw.figure}). ` +
+ `\n\nFlat solid color regions only. Hard razor-sharp silhouette edges. Single-layer screenprint register. ` +
+ `Hand-painted-silk scenic in the style of Zuber & Cie, Gracie, and de Gournay. ` +
+ `No watermark, no text, no border, no signature.`
+ );
+
+ process.stdout.write(`[${i+1}/6] ${cw.slug} → `);
+ const t0 = Date.now();
+ const body = {
+ contents: [{
+ parts: [
+ { inline_data: { mime_type: 'image/png', data: imageB64 } },
+ { text: editPrompt },
+ ],
+ }],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ };
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (!r.ok) { console.log(`HTTP ${r.status}`); continue; }
+ const j = await r.json();
+ try {
+ const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
+ logGemini(j, { app: 'wallco-ai', note: '39055-colorway-' + cw.slug, model: 'gemini-2.5-flash-image' });
+ } catch {}
+ const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+ const data = part?.inline_data?.data || part?.inlineData?.data;
+ if (!data) {
+ const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
+ console.log(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 120)}`);
+ continue;
+ }
+
+ const newSeed = crypto.randomInt(1, 2 ** 31 - 1);
+ const filename = `gracie_${cw.slug}_${Date.now()}_${newSeed}.png`;
+ const outPath = path.join(OUT_DIR, filename);
+ fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+
+ const py = spawnSync('python3', ['-c', `
+from PIL import Image
+from collections import Counter
+import json, sys
+img = Image.open(sys.argv[1]).convert('RGB')
+img.thumbnail((300, 300))
+pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
+pixels = list(pal.getdata())
+cnt = Counter(pixels)
+total = sum(cnt.values())
+palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
+print(json.dumps(palette))
+`, outPath], { encoding: 'utf8' });
+ let palette = []; let dominant = cw.ground;
+ try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex || cw.ground; } catch {}
+
+ const ins = psql(`
+INSERT INTO spoon_all_designs
+ (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+ image_url, local_path, dominant_hex, palette, motifs, tags, category, is_published)
+VALUES
+ ('mural_panel', 'wallco.ai', ${w}, ${h}, ${panels || 20},
+ 'gemini-2.5-flash-image-edit',
+ ${esc(`Gracie-language colorway ${cw.slug} from #${SRC_ID}: ` + editPrompt.slice(0, 1200))},
+ ${newSeed},
+ '/designs/img/by-id/__NEW__',
+ ${esc(outPath)},
+ ${esc(dominant)},
+ ${esc(JSON.stringify(palette))}::jsonb,
+ ${motifs || 'NULL'},
+ ${tags || 'NULL'},
+ 'cactus-pine-scenic',
+ TRUE)
+RETURNING id`);
+ const newId = parseInt(ins, 10);
+ psql(`UPDATE spoon_all_designs SET image_url = '/designs/img/by-id/${newId}', is_published = TRUE WHERE id = ${newId}`);
+ newIds.push(newId);
+ console.log(`#${newId} (${((Date.now() - t0)/1000).toFixed(1)}s)`);
+ }
+
+ console.log(`\nDone — ${newIds.length}/${COLORWAYS.length} ok`);
+ console.log('New IDs: ' + JSON.stringify(newIds));
+})();
diff --git a/scripts/gemini-rerender-weak3.js b/scripts/gemini-rerender-weak3.js
new file mode 100644
index 0000000..24e48db
--- /dev/null
+++ b/scripts/gemini-rerender-weak3.js
@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+/**
+ * Re-render the 3 weakest cactus-pine + cactus-11ft + tree-mural designs per
+ * the 2026-05-21 interior-designer panel review:
+ *
+ * #39062 desert-coral redo (2.2/5) → redo in flat Gracie-language with
+ * coral as the SINGLE ink, source = #39055 (winning aesthetic), not the
+ * photoreal Pass-2 source
+ * #38848 B&W agave centered radial (2.5/5) → asymmetric crop, second ink
+ * tone (sepia or indigo wash) so it can sit in a room with color
+ * #38908 saguaro forest dusk (2.8/5) → desaturate sky 30%, replace cobalt-
+ * orange dusk with single-tone wash (ash-rose / oyster-pink)
+ *
+ * Existing rows go unpublished; new rows replace them in the same category.
+ *
+ * Cost: 3 × ~$0.04 = ~$0.12.
+ */
+'use strict';
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { execSync, spawnSync } = require('child_process');
+
+const KEY = process.env.GEMINI_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
+const DB = 'dw_unified';
+function psql(sql) { return execSync(`psql ${DB} -At -q`, { input: sql, encoding: 'utf8' }).trim(); }
+function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
+
+const JOBS = [
+ {
+ src_id: 39055, // use the WINNING aesthetic as the base, not the failed #39046
+ replaces: 39062,
+ label: 'desert-coral flat silhouette',
+ edit_prompt: (
+ 'Recolor this hand-painted cactus and Monterey pine scenic to a desert-coral colorway. ' +
+ 'Preserve the EXACT composition, silhouettes, layout, and flat hand-painted-silk register. ' +
+ 'Do NOT re-introduce photoreal shading, gradients, or atmospheric perspective. ' +
+ 'Ground (sky, mountains, ground plane all share the same flat tone): sun-baked desert-coral (#B85440). ' +
+ 'Figure (all silhouettes — cactus, pine trunks, pine canopies, mountain edges): tortilla-bone cream (#F2EADB). ' +
+ 'Single-layer hand-blocked silkscreen, hard razor-sharp edges, Zuber/Gracie/de Gournay register. ' +
+ 'No watermark, no text, no border.'
+ ),
+ },
+ {
+ src_id: 38848,
+ replaces: 38848,
+ label: 'B&W agave off-center + warm sepia second ink',
+ edit_prompt: (
+ 'Re-compose this monochrome agave specimen into an OFF-CENTER vignette: anchor the leaf cluster ' +
+ 'to the right third of the canvas, leave the left two-thirds as flat negative sky space. The plant ' +
+ 'should no longer be radially symmetric or dead-center — it should feel like a botanical-screen ' +
+ 'composition seen from the side. ' +
+ 'Add a SECOND ink tone: a warm sepia-brown wash (#7A4A28) for the inner leaf detail, on top of ' +
+ 'the deep black silhouette already present. The flat background stays bone-cream (#F2EADB). ' +
+ 'Keep the hand-painted screenprint register. Hard edges. No gradient sky. ' +
+ 'Reads as a Zuber-style botanical panel, not a museum poster. No watermark, no text.'
+ ),
+ },
+ {
+ src_id: 38908,
+ replaces: 38908,
+ label: 'saguaro dusk in oyster-pink single-tone wash',
+ edit_prompt: (
+ 'Recolor this dusk saguaro forest mural. REMOVE the cobalt-blue-to-hot-orange dusk gradient sky ' +
+ 'entirely and replace with a single flat tone of warm oyster-pink (#D4A89A) covering the entire ' +
+ 'sky area edge to edge — no gradient, no clouds, no sun glow. ' +
+ 'Distant mountains become a single flat silhouette band in deep wine-mauve (#5C3540). ' +
+ 'Cactus and foreground scrub in a deeper sepia-charcoal (#3A2A1C). ' +
+ 'Drop overall saturation 30 percent. Push toward flat hand-painted-silk register — single ' +
+ 'screen-print pass per layer, hard edges, no atmospheric perspective. ' +
+ 'Reads as a quiet Sonoran-twilight scenic, not a postcard dusk. No watermark, no text.'
+ ),
+ },
+];
+
+(async function main() {
+ console.log(`Re-rendering ${JOBS.length} weakest murals via Gemini image-edit\n`);
+ const newIds = [];
+
+ for (const job of JOBS) {
+ const row = psql(`SELECT id, local_path, width_in, height_in, panels, palette, motifs, tags, category
+ FROM spoon_all_designs WHERE id = ${job.src_id}`);
+ if (!row) { console.log(`SKIP src #${job.src_id} missing`); continue; }
+ const [_id, srcPath, w, h, panels, _pal, motifs, tags, category] = row.split('|');
+ if (!fs.existsSync(srcPath)) { console.log(`SKIP src PNG ${srcPath} missing`); continue; }
+
+ process.stdout.write(`[${job.label}] src=#${job.src_id} → `);
+ const imageB64 = fs.readFileSync(srcPath).toString('base64');
+ const body = {
+ contents: [{ parts: [
+ { inline_data: { mime_type: 'image/png', data: imageB64 } },
+ { text: job.edit_prompt },
+ ]}],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ };
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
+ const t0 = Date.now();
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (!r.ok) { console.log(`HTTP ${r.status}`); continue; }
+ const j = await r.json();
+ try {
+ const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
+ logGemini(j, { app: 'wallco-ai', note: 'rerender-weak-' + job.replaces, model: 'gemini-2.5-flash-image' });
+ } catch {}
+ const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+ const data = part?.inline_data?.data || part?.inlineData?.data;
+ if (!data) {
+ const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
+ console.log(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 120)}`);
+ continue;
+ }
+
+ const newSeed = crypto.randomInt(1, 2 ** 31 - 1);
+ const filename = `rerender_${job.replaces}_${Date.now()}_${newSeed}.png`;
+ const outPath = path.join(OUT_DIR, filename);
+ fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+
+ const py = spawnSync('python3', ['-c', `
+from PIL import Image
+from collections import Counter
+import json, sys
+img = Image.open(sys.argv[1]).convert('RGB')
+img.thumbnail((300, 300))
+pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
+pixels = list(pal.getdata())
+cnt = Counter(pixels)
+total = sum(cnt.values())
+palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
+print(json.dumps(palette))
+`, outPath], { encoding: 'utf8' });
+ let palette = []; let dominant = null;
+ try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex; } catch {}
+
+ const ins = psql(`
+INSERT INTO spoon_all_designs
+ (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+ image_url, local_path, dominant_hex, palette, motifs, tags, category, is_published)
+VALUES
+ ('mural_panel', 'wallco.ai', ${w}, ${h}, ${panels || 1},
+ 'gemini-2.5-flash-image-edit',
+ ${esc('Re-render of #' + job.replaces + ' (' + job.label + '): ' + job.edit_prompt.slice(0, 1200))},
+ ${newSeed},
+ '/designs/img/by-id/__NEW__',
+ ${esc(outPath)},
+ ${esc(dominant)},
+ ${esc(JSON.stringify(palette))}::jsonb,
+ ${motifs || 'NULL'},
+ ${tags || 'NULL'},
+ ${esc(category)},
+ TRUE)
+RETURNING id`);
+ const newId = parseInt(ins, 10);
+ psql(`UPDATE spoon_all_designs SET image_url = '/designs/img/by-id/${newId}', is_published = TRUE WHERE id = ${newId}`);
+ psql(`UPDATE spoon_all_designs SET is_published = FALSE WHERE id = ${job.replaces}`);
+ newIds.push(newId);
+ console.log(`#${newId} (${((Date.now() - t0)/1000).toFixed(1)}s) · old #${job.replaces} unpublished`);
+ }
+
+ console.log(`\nDone — ${newIds.length}/${JOBS.length} ok. New IDs: ${JSON.stringify(newIds)}`);
+})();
diff --git a/server.js b/server.js
index 2e8deea..04f8cc3 100644
--- a/server.js
+++ b/server.js
@@ -7605,6 +7605,13 @@ ${htmlHeader('/designs')}
#scale-viewport .mural-hint { position:absolute; bottom:8px; left:10px; right:10px; display:flex; justify-content:space-between; align-items:center; font:10px ui-monospace,Menlo,monospace; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.7); letter-spacing:.04em; pointer-events:none; opacity:.85; }
#scale-viewport .mural-hint .zoom-pill { padding:2px 7px; background:rgba(0,0,0,.42); border-radius:999px; pointer-events:auto; }
</style>
+ <!-- N×M tiles callout — single-line readout of how many roll-widths
+ and pattern-repeats the customer's wall will hold. Updates live
+ from readWallSpec() on every wall/roll/repeat change. Hidden in
+ mural mode (level=0) where the concept doesn't apply. -->
+ <div id="tiles-readout" style="display:none;font:12px var(--sans);color:var(--ink-soft,#555);margin:8px 2px 6px;text-align:center;letter-spacing:.02em">
+ Your <strong id="tr-wall">10 × 9 ft</strong> wall will hold <strong id="tr-grid">5 tiles wide × 9 repeats tall</strong> <span style="color:var(--ink-faint,#999)">(5 drops at <span id="tr-roll">24"</span> roll · <span id="tr-repeat">24"</span> repeat)</span>
+ </div>
<div id="scale-viewport" style="width:100%;aspect-ratio:20/11;border:1px solid var(--line);border-radius:8px;overflow:hidden;background-image:url('${design.image_url}');background-repeat:no-repeat;background-size:cover;background-position:50% 50%;position:relative;user-select:none;-webkit-user-select:none">
<div class="mural-hint" id="mural-hint" style="display:none">
<span>20 ft × 11 ft mural · drag to position · scroll to zoom · double-click to reset</span>
@@ -7757,6 +7764,29 @@ ${htmlHeader('/designs')}
if (soldBy) soldBy.innerHTML = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
const hint = document.getElementById('scale-width-hint');
if (hint) hint.textContent = isMural ? '36" panel × 11 ft, sold per panel' : '24" wide, sold per roll (27 ft)';
+ // N×M tiles textual readout (UX-designer panel review 2026-05-21):
+ // at zoom != 0.5× the viewport shows a magnified excerpt, not the
+ // full wall — customer cannot count tiles visually. This text
+ // spells it out: "Your 10×9 ft wall will hold 5 tiles × 9 repeats".
+ const trBlock = document.getElementById('tiles-readout');
+ if (trBlock) {
+ if (isMural) {
+ trBlock.style.display = 'none';
+ } else {
+ trBlock.style.display = '';
+ var sp2 = readWallSpec();
+ var tilesX = Math.ceil(sp2.wallWIn / sp2.rollW);
+ var repeatsY = Math.ceil(sp2.wallHIn / sp2.repeatIn);
+ var trWall = document.getElementById('tr-wall');
+ var trGrid = document.getElementById('tr-grid');
+ var trRoll = document.getElementById('tr-roll');
+ var trRep = document.getElementById('tr-repeat');
+ if (trWall) trWall.textContent = sp2.wallWFt + ' × ' + sp2.wallHFt + ' ft';
+ if (trGrid) trGrid.textContent = tilesX + ' tiles wide × ' + repeatsY + ' repeats tall';
+ if (trRoll) trRoll.textContent = sp2.rollW + '"';
+ if (trRep) trRep.textContent = sp2.repeatIn + '"';
+ }
+ }
}
if (btn) btn.addEventListener('click', () => { level = (level + 1) % LEVELS.length; apply(); });
dots.forEach(d => d.addEventListener('click', (e) => { level = parseInt(e.currentTarget.dataset.level,10) || 0; apply(); }));
← 3ddc65b feat(cactus+murals): 48 cactus-11ft + 32 tree-murals + 6 cac
·
back to Wallco Ai
·
regen 10 BLEED_GHOST_REGEN lips via SDXL→Gemini 2-pass — new 38532bd →