[object Object]

← back to Designer Wallcoverings

WallQuest onboard: swatch DL + local qwen2.5vl enrich + rooms + settlement gate

8de32f959893dfcc753fa3919a1b7420bad269ec · 2026-07-09 23:08:09 -0700 · Steve

- dl-swatches.cjs: download swatches, backfill local_image_path (187, 0 bad)
- enrich-book.py: local qwen2.5vl vision enrich ($0), luxury desc + colors + hex + styles
- 187 patch-quilt rooms generated ($0); room_setting_images backfilled
- Daisy v2 settlement gate: 7 florals/leaves flagged REVIEW (stay DRAFT), 45 clear
- image dirs gitignored

Files touched

Diff

commit 8de32f959893dfcc753fa3919a1b7420bad269ec
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 9 23:08:09 2026 -0700

    WallQuest onboard: swatch DL + local qwen2.5vl enrich + rooms + settlement gate
    
    - dl-swatches.cjs: download swatches, backfill local_image_path (187, 0 bad)
    - enrich-book.py: local qwen2.5vl vision enrich ($0), luxury desc + colors + hex + styles
    - 187 patch-quilt rooms generated ($0); room_setting_images backfilled
    - Daisy v2 settlement gate: 7 florals/leaves flagged REVIEW (stay DRAFT), 45 clear
    - image dirs gitignored
---
 .../daisy-v2-onboard-viewer/.gitignore             |  2 ++
 scripts/wallquest-refresh/dl-swatches.cjs          | 35 +++++++++++++++++++
 scripts/wallquest-refresh/enrich-book.py           | 39 ++++++++++++++++++++++
 .../lillian-august-onboard-viewer/.gitignore       |  2 ++
 4 files changed, 78 insertions(+)

diff --git a/scripts/wallquest-refresh/daisy-v2-onboard-viewer/.gitignore b/scripts/wallquest-refresh/daisy-v2-onboard-viewer/.gitignore
new file mode 100644
index 00000000..b81752c9
--- /dev/null
+++ b/scripts/wallquest-refresh/daisy-v2-onboard-viewer/.gitignore
@@ -0,0 +1,2 @@
+img/
+rooms-local/
diff --git a/scripts/wallquest-refresh/dl-swatches.cjs b/scripts/wallquest-refresh/dl-swatches.cjs
new file mode 100644
index 00000000..4d68d48c
--- /dev/null
+++ b/scripts/wallquest-refresh/dl-swatches.cjs
@@ -0,0 +1,35 @@
+// Download the first product image per SKU -> local swatch, backfill local_image_path.
+// Env: TABLE, VIEW (viewer dir; writes img/{dw_sku}.jpg). WallQuest thumbs aren't hotlink-blocked.
+const fs = require('fs'), https = require('https'), http = require('http'), { execSync } = require('child_process');
+const TABLE = process.env.TABLE, VIEW = process.env.VIEW;
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const IMG = `${VIEW}/img`; fs.mkdirSync(IMG, { recursive: true });
+const psql = sql => execSync(`psql "${CONN}" -tAF'|' -c "${sql.replace(/"/g,'\\"')}"`, { encoding: 'utf8' });
+const get = url => new Promise((res, rej) => {
+  const mod = url.startsWith('https') ? https : http;
+  const req = mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.wallquest.com/' }, timeout: 30000 }, r => {
+    if (r.statusCode >= 300 && r.headers.location) return get(new URL(r.headers.location, url).href).then(res, rej);
+    if (r.statusCode !== 200) { r.resume(); return rej(new Error('http ' + r.statusCode)); }
+    const bufs = []; r.on('data', c => bufs.push(c)); r.on('end', () => res(Buffer.concat(bufs)));
+  });
+  req.on('error', rej); req.on('timeout', () => { req.destroy(); rej(new Error('timeout')); });
+});
+
+(async () => {
+  const rows = psql(`SELECT dw_sku, COALESCE(all_images::text,'[]') FROM ${TABLE} WHERE all_images IS NOT NULL`).trim().split('\n').filter(Boolean);
+  let ok = 0, bad = 0; const sqls = [];
+  for (const line of rows) {
+    const [sku, imgsRaw] = line.split('|');
+    let imgs = []; try { imgs = JSON.parse(imgsRaw); } catch {}
+    const url = imgs.find(u => u && /jpe?g|png/i.test(u));
+    if (!url) { bad++; continue; }
+    const dest = `${IMG}/${sku}.jpg`, rel = `img/${sku}.jpg`;
+    if (!fs.existsSync(dest)) {
+      try { const buf = await get(url); if (buf.length < 2000) { bad++; continue; } fs.writeFileSync(dest, buf); ok++; }
+      catch (e) { bad++; continue; }
+    } else ok++;
+    sqls.push(`UPDATE ${TABLE} SET local_image_path='${JSON.stringify([rel])}'::jsonb WHERE dw_sku='${sku}';`);
+  }
+  if (sqls.length) { fs.writeFileSync('/tmp/_dl.sql', sqls.join('\n')); execSync(`psql "${CONN}" -q -f /tmp/_dl.sql`); }
+  console.log(`${TABLE}: swatches ok=${ok} bad=${bad} -> ${IMG}`);
+})();
diff --git a/scripts/wallquest-refresh/enrich-book.py b/scripts/wallquest-refresh/enrich-book.py
new file mode 100644
index 00000000..83109b3f
--- /dev/null
+++ b/scripts/wallquest-refresh/enrich-book.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# Local vision enrich (qwen2.5vl via Ollama, $0): per SKU, read the swatch -> 2-sentence luxury
+# description + dominant color names + hex + styles. Updates catalog. Resumable (skips enriched).
+import base64, json, os, subprocess, urllib.request, sys
+TABLE = os.environ['TABLE']; VIEW = os.environ['VIEW']
+CONN = os.environ.get('DATABASE_URL', 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
+MODEL = 'qwen2.5vl:7b'
+def psql(sql): return subprocess.run(['psql', CONN, '-tAF|', '-c', sql], capture_output=True, text=True).stdout
+def esc(s): return "'" + str(s).replace("'", "''") + "'" if s is not None else 'NULL'
+
+rows = [l.split('|') for l in psql(
+  f"SELECT dw_sku, COALESCE(pattern_name,''), COALESCE(color_name,''), COALESCE(material,''), COALESCE((local_image_path::jsonb)->>0,'') "
+  f"FROM {TABLE} WHERE (ai_description IS NULL OR ai_description='') ORDER BY dw_sku").strip().split('\n') if '|' in l]
+print(f'{TABLE}: {len(rows)} to enrich', flush=True)
+done = 0
+for sku, pat, color, mat, img in rows:
+    p = f'{VIEW}/{img}'
+    if not img or not os.path.exists(p): continue
+    prompt = (f"This is a {mat} wallcovering swatch named '{pat}' in colorway '{color}'. Write a 2-sentence "
+      f"luxury product description for an interior-design audience (no brand names, no vendor names). Then list "
+      f"the dominant colors, a hex for the main color, and 2-3 interior styles it suits. Respond ONLY JSON: "
+      f'{{"description":"...","colors":["..."],"hex":"#RRGGBB","styles":["..."]}}')
+    try:
+        body = json.dumps({"model": MODEL, "prompt": prompt, "images": [base64.b64encode(open(p,'rb').read()).decode()],
+                           "format": "json", "stream": False, "options": {"temperature": 0.2}}).encode()
+        r = json.load(urllib.request.urlopen(urllib.request.Request(
+            "http://localhost:11434/api/generate", body, {"Content-Type": "application/json"}), timeout=240))
+        j = json.loads(r["response"])
+        desc = (j.get('description') or '').replace('Wallpaper', 'Wallcovering')
+        sql = (f"UPDATE {TABLE} SET ai_description={esc(desc)}, "
+               f"ai_colors={esc(json.dumps(j.get('colors') or []))}::jsonb, "
+               f"color_hex={esc(j.get('hex') or '')}, ai_styles={esc(json.dumps(j.get('styles') or []))}::jsonb, "
+               f"updated_at=now() WHERE dw_sku={esc(sku)}")
+        subprocess.run(['psql', CONN, '-q', '-c', sql], capture_output=True)
+        done += 1
+        if done % 20 == 0: print(f'  {done}/{len(rows)}', flush=True)
+    except Exception as e:
+        print(f'  ERR {sku}: {str(e)[:60]}', flush=True)
+print(f'{TABLE}: ENRICHED {done}', flush=True)
diff --git a/scripts/wallquest-refresh/lillian-august-onboard-viewer/.gitignore b/scripts/wallquest-refresh/lillian-august-onboard-viewer/.gitignore
new file mode 100644
index 00000000..b81752c9
--- /dev/null
+++ b/scripts/wallquest-refresh/lillian-august-onboard-viewer/.gitignore
@@ -0,0 +1,2 @@
+img/
+rooms-local/

← 3483fed8 WallQuest real-textile onboard pipeline: normalize + load +  ·  back to Designer Wallcoverings  ·  catalog-push: diff-based redesign w/ two-bucket safety (sync 65004b34 →