← back to Wallco Ai
hero: 2048² Lanczos-upscaled room mockups; heroImageUrl prefers _hero.png
88d80d73d676aecf31a63f9e67d26d692ba9413d · 2026-05-28 12:20:06 -0700 · Steve Abrams
The upstream room renderer (45.61.58.125:3075) hard-codes 1024² output. On a
1920px hero with contain mode that's noticeably soft on Retina displays — Steve
asked where the hi-res hero images were ('where are the high res room setting
hero images????' 2026-05-28).
scripts/upscale-room-heroes.py: PIL Lanczos 2x of every data/rooms/design_*.png
into a sibling _hero.png at 2048². Idempotent + parallel (11 workers). 400/400
existing rooms upscaled in 97s, ~4MB/file, zero errors. Re-run anytime new
rooms land; --force overwrites.
server.js heroImageUrl(): _ROOM_HERO_SET in-memory index of basenames-with-_hero,
rebuilt at boot AND on /admin/reload-designs. Hero URL prefers _hero.png when
present, falls back to 1024² .png otherwise. Product cards (renderProduct +
card-room) intentionally untouched — they render at ~280px wide and don't
benefit from the bigger asset.
Verified: all 6 live hero slots on / now serve /designs/room/<id>_<room>_hero.png
at (2048, 2048), http 200, 3.1-4.5 MB each.
Files touched
M data/cactus-decisions.jsonlA scripts/upscale-room-heroes.pyM server.js
Diff
commit 88d80d73d676aecf31a63f9e67d26d692ba9413d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 12:20:06 2026 -0700
hero: 2048² Lanczos-upscaled room mockups; heroImageUrl prefers _hero.png
The upstream room renderer (45.61.58.125:3075) hard-codes 1024² output. On a
1920px hero with contain mode that's noticeably soft on Retina displays — Steve
asked where the hi-res hero images were ('where are the high res room setting
hero images????' 2026-05-28).
scripts/upscale-room-heroes.py: PIL Lanczos 2x of every data/rooms/design_*.png
into a sibling _hero.png at 2048². Idempotent + parallel (11 workers). 400/400
existing rooms upscaled in 97s, ~4MB/file, zero errors. Re-run anytime new
rooms land; --force overwrites.
server.js heroImageUrl(): _ROOM_HERO_SET in-memory index of basenames-with-_hero,
rebuilt at boot AND on /admin/reload-designs. Hero URL prefers _hero.png when
present, falls back to 1024² .png otherwise. Product cards (renderProduct +
card-room) intentionally untouched — they render at ~280px wide and don't
benefit from the bigger asset.
Verified: all 6 live hero slots on / now serve /designs/room/<id>_<room>_hero.png
at (2048, 2048), http 200, 3.1-4.5 MB each.
---
data/cactus-decisions.jsonl | 31 ++++++++++++++++
scripts/upscale-room-heroes.py | 82 ++++++++++++++++++++++++++++++++++++++++++
server.js | 27 ++++++++++++--
3 files changed, 138 insertions(+), 2 deletions(-)
diff --git a/data/cactus-decisions.jsonl b/data/cactus-decisions.jsonl
index d034102..b7a84dd 100644
--- a/data/cactus-decisions.jsonl
+++ b/data/cactus-decisions.jsonl
@@ -9474,3 +9474,34 @@
{"ts": "2026-05-28T19:07:14.536919Z", "id": 22292, "action": "orphan-cleanup"}
{"ts": "2026-05-28T19:07:14.536919Z", "id": 22488, "action": "orphan-cleanup"}
{"ts": "2026-05-28T19:07:14.536919Z", "id": 22750, "action": "orphan-cleanup"}
+{"ts":"2026-05-28T19:19:37.142Z","id":27370,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.254Z","id":27384,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.359Z","id":25416,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.474Z","id":27397,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.579Z","id":25451,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.699Z","id":25434,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.823Z","id":27408,"action":"bad"}
+{"ts":"2026-05-28T19:19:37.957Z","id":27339,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.084Z","id":27356,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.207Z","id":27464,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.348Z","id":25465,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.464Z","id":25383,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.592Z","id":25377,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.741Z","id":27323,"action":"bad"}
+{"ts":"2026-05-28T19:19:38.886Z","id":27449,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.030Z","id":25001,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.157Z","id":25035,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.287Z","id":25399,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.414Z","id":25075,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.545Z","id":25118,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.671Z","id":25190,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.802Z","id":25230,"action":"bad"}
+{"ts":"2026-05-28T19:19:39.948Z","id":25324,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.080Z","id":25360,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.210Z","id":27309,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.353Z","id":27435,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.469Z","id":24931,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.601Z","id":25338,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.747Z","id":25476,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.877Z","id":27424,"action":"bad"}
+{"ts":"2026-05-28T19:19:40.998Z","id":25344,"action":"bad"}
diff --git a/scripts/upscale-room-heroes.py b/scripts/upscale-room-heroes.py
new file mode 100644
index 0000000..1142ebf
--- /dev/null
+++ b/scripts/upscale-room-heroes.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+"""
+upscale-room-heroes.py — 1024² → 2048² Lanczos upscale of room mockups for hero use.
+
+The upstream room renderer (45.61.58.125:3075/api/generate-room) hard-codes 1024²
+output. On wide heroes that's too small to look crisp, so we Lanczos-2x each room
+PNG into a `_hero.png` sibling. `server.js heroImageUrl()` prefers the `_hero`
+variant when present, falls back to the 1024² for non-hero card uses.
+
+Idempotent: skips any file whose `_hero` sibling already exists with the same
+mtime-or-later. Runs in parallel.
+
+Usage:
+ python3 scripts/upscale-room-heroes.py # walk + upscale missing
+ python3 scripts/upscale-room-heroes.py --force # overwrite existing
+ python3 scripts/upscale-room-heroes.py --limit 50 # cap (smoke test)
+"""
+import os, sys, glob, time, argparse
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from PIL import Image
+
+ROOMS_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'rooms')
+TARGET = 2048 # 2x the upstream 1024²
+
+def is_hero(p):
+ return p.endswith('_hero.png')
+
+def hero_path(p):
+ base, ext = os.path.splitext(p)
+ return base + '_hero' + ext
+
+def upscale_one(src, force=False):
+ dst = hero_path(src)
+ if os.path.exists(dst) and not force:
+ try:
+ if os.path.getmtime(dst) >= os.path.getmtime(src):
+ return (src, 'skip', 0)
+ except OSError:
+ pass
+ t0 = time.time()
+ try:
+ im = Image.open(src).convert('RGB')
+ # If the source is already ≥TARGET, just save a copy as the hero variant
+ # rather than down+upsampling.
+ if min(im.size) >= TARGET:
+ im.save(dst, 'PNG', optimize=True)
+ else:
+ up = im.resize((TARGET, TARGET), Image.LANCZOS)
+ up.save(dst, 'PNG', optimize=True)
+ return (src, 'ok', round(time.time() - t0, 2))
+ except Exception as e:
+ return (src, f'err:{e}', round(time.time() - t0, 2))
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--force', action='store_true')
+ ap.add_argument('--limit', type=int, default=None)
+ ap.add_argument('--workers', type=int, default=max(2, (os.cpu_count() or 4) - 1))
+ args = ap.parse_args()
+
+ all_pngs = sorted(glob.glob(os.path.join(ROOMS_DIR, 'design_*.png')))
+ sources = [p for p in all_pngs if not is_hero(p)]
+ if args.limit:
+ sources = sources[:args.limit]
+ print(f'[upscale] {len(sources)} candidate room PNGs · target={TARGET}² · workers={args.workers} · force={args.force}')
+
+ ok = skipped = errored = 0
+ t0 = time.time()
+ with ProcessPoolExecutor(max_workers=args.workers) as ex:
+ futs = {ex.submit(upscale_one, p, args.force): p for p in sources}
+ for i, fut in enumerate(as_completed(futs), 1):
+ src, status, dt = fut.result()
+ if status == 'ok': ok += 1
+ elif status == 'skip': skipped += 1
+ else: errored += 1; print(f' [err] {os.path.basename(src)} → {status}')
+ if i % 25 == 0 or i == len(sources):
+ rate = i / max(time.time() - t0, 1e-6)
+ print(f' [{i}/{len(sources)}] ok={ok} skip={skipped} err={errored} · {rate:.1f}/s')
+ print(f'[done] ok={ok} skip={skipped} err={errored} · {round(time.time()-t0,1)}s')
+
+if __name__ == '__main__':
+ main()
diff --git a/server.js b/server.js
index efaadc6..7cd7b15 100644
--- a/server.js
+++ b/server.js
@@ -849,7 +849,10 @@ app.post('/admin/reload-designs', (req, res) => {
// can't outlive the new catalog state. ETags rebuild against the new CATALOG_LAST_MODIFIED.
_byColorCacheVer++;
_byColorCache.clear();
- res.json({ ok: true, before, after: DESIGNS.length, catalog_last_modified: CATALOG_LAST_MODIFIED, by_color_cache_ver: _byColorCacheVer });
+ // Refresh the hi-res `_hero.png` index so any newly-upscaled room files
+ // (scripts/upscale-room-heroes.py) take effect without a pm2 reload.
+ if (typeof rebuildRoomHeroSet === 'function') rebuildRoomHeroSet();
+ res.json({ ok: true, before, after: DESIGNS.length, catalog_last_modified: CATALOG_LAST_MODIFIED, by_color_cache_ver: _byColorCacheVer, hero_set_size: (typeof _ROOM_HERO_SET !== 'undefined') ? _ROOM_HERO_SET.size : 0 });
});
// ── Inspirations gallery (admin-only) ─────────────────────────────────────
@@ -4714,9 +4717,29 @@ const HAMBURGER_JS = `
// "use room settings for hero images". Room files served by the static route
// at /designs/room/ from data/rooms/design_<id>_<room>.png. Falls back to the
// flat tile (d.image_url) for designs with no rooms yet.
+// 2026-05-28: prefer the 2048² `_hero.png` Lanczos-upscale variant when present
+// (scripts/upscale-room-heroes.py builds these from the 1024² upstream output).
+// _ROOM_HERO_SET is a Set of basenames-without-extension for O(1) lookup; rebuilt
+// on server boot so newly-upscaled files take effect on next reload-designs hit.
+const _ROOMS_DIR = path.join(__dirname, 'data', 'rooms');
+let _ROOM_HERO_SET = new Set();
+function rebuildRoomHeroSet() {
+ try {
+ _ROOM_HERO_SET = new Set(
+ fs.readdirSync(_ROOMS_DIR)
+ .filter(n => n.endsWith('_hero.png'))
+ .map(n => n.slice(0, -4)) // strip ".png" — base ends in "_hero"
+ );
+ } catch (e) { _ROOM_HERO_SET = new Set(); }
+}
+rebuildRoomHeroSet();
function heroImageUrl(d) {
if (Array.isArray(d.room_mockups) && d.room_mockups.length) {
- return '/designs/room/design_' + d.id + '_' + d.room_mockups[0] + '.png';
+ const base = 'design_' + d.id + '_' + d.room_mockups[0];
+ if (_ROOM_HERO_SET.has(base + '_hero')) {
+ return '/designs/room/' + base + '_hero.png';
+ }
+ return '/designs/room/' + base + '.png';
}
return d.image_url;
}
← cf819f1 snapshot: lift pil-edge-heal block for mural/scenic categori
·
back to Wallco Ai
·
hero: room slides use background-size:cover (full-width) ins 09c554e →