← back to Dw Pairs Well
auto-save: 2026-06-25T23:37:59 (1 files) — tools/
c76eef67c2ff6dcd8c76e6a42e5fec335480e834 · 2026-06-25 23:38:08 -0700 · Steve Abrams
Files touched
A tools/pattern-grouping-dryrun.jsA tools/pattern-grouping-report.jsonA tools/pattern-image-verify.py
Diff
commit c76eef67c2ff6dcd8c76e6a42e5fec335480e834
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 25 23:38:08 2026 -0700
auto-save: 2026-06-25T23:37:59 (1 files) — tools/
---
tools/pattern-grouping-dryrun.js | 117 +++++
tools/pattern-grouping-report.json | 854 +++++++++++++++++++++++++++++++++++++
tools/pattern-image-verify.py | 107 +++++
3 files changed, 1078 insertions(+)
diff --git a/tools/pattern-grouping-dryrun.js b/tools/pattern-grouping-dryrun.js
new file mode 100644
index 0000000..a1ae3af
--- /dev/null
+++ b/tools/pattern-grouping-dryrun.js
@@ -0,0 +1,117 @@
+#!/usr/bin/env node
+// WS-1 dry-run (READ-ONLY): compare candidate pattern-grouping strategies on the
+// live dw_unified mirror so /dtd can commit a grouping-key choice on real numbers.
+// Writes NOTHING. Outputs a report to stdout + tools/pattern-grouping-report.json.
+//
+// node tools/pattern-grouping-dryrun.js
+//
+// Strategies compared (all scoped WITHIN vendor to avoid cross-vendor collisions):
+// T0 exact title
+// T1 normalized title (strip " | vendor" suffix, product-type noise, punctuation)
+// T2 T1 + strip color tokens (so "Albany Linen-Beige" and "-Cedar" collapse)
+// PN pattern_name (baseline — known unreliable; quantifies how bad)
+require('dotenv').config();
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+const { COLORS } = require('../lib/classify');
+
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+
+// product-type / material noise to drop from titles when deriving a pattern key
+const NOISE = [
+ 'wallcovering','wallcoverings','wallpaper','wall covering','fabric','fabrics',
+ 'durable vinyl','vinyl','grasscloth','print','commercial','residential',
+ 'type ii','type iii','mural','wall mural','peel and stick','peel-and-stick',
+ 'self adhesive','self-adhesive','by phillipe romano','phillipe romano'
+];
+// color tokens = the classify COLOR set + common modifiers
+const COLOR_WORDS = new Set([...COLORS,
+ 'light','dark','deep','pale','soft','warm','cool','muted','bright','antique',
+ 'metallic','natural','neutral','multi','multicolor','multicolour'
+]);
+
+function stripVendorSuffix(title) {
+ // "Name ... | Vendor" → "Name ..."
+ const i = title.indexOf('|');
+ return (i === -1 ? title : title.slice(0, i)).trim();
+}
+function normTitle(title) {
+ let s = stripVendorSuffix(title).toLowerCase();
+ for (const n of NOISE) s = s.split(n).join(' ');
+ s = s.replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
+ return s;
+}
+function stripColors(normalized) {
+ const kept = normalized.split(' ').filter(w => w && !COLOR_WORDS.has(w));
+ return kept.join(' ').trim() || normalized; // never empty-out a title
+}
+
+function summarize(label, keyFn, rows) {
+ const groups = new Map(); // key -> {count, vendor, sampleTitles:Set}
+ let keyed = 0;
+ for (const r of rows) {
+ const k = keyFn(r);
+ if (!k) continue;
+ keyed++;
+ const gk = (r.vendor || '∅') + '::' + k;
+ let g = groups.get(gk);
+ if (!g) { g = { count: 0, vendor: r.vendor, key: k, samples: [] }; groups.set(gk, g); }
+ g.count++;
+ if (g.samples.length < 3) g.samples.push(r.title);
+ }
+ const arr = [...groups.values()];
+ const multi = arr.filter(g => g.count >= 2);
+ const productsInMulti = multi.reduce((a, g) => a + g.count, 0);
+ arr.sort((a, b) => b.count - a.count);
+ return {
+ label,
+ products_keyed: keyed,
+ distinct_groups: arr.length,
+ multi_colorway_groups: multi.length,
+ products_in_multi_groups: productsInMulti,
+ pct_collapsed: ((productsInMulti - multi.length) / keyed * 100).toFixed(1) + '%',
+ avg_group_size: (keyed / arr.length).toFixed(2),
+ top20: arr.slice(0, 20).map(g => ({ n: g.count, vendor: g.vendor, key: g.key, eg: g.samples })),
+ // over-grouping suspects = very large groups (could be legit like Fine Stripes×76 OR junk)
+ huge_groups_gt60: arr.filter(g => g.count > 60).length
+ };
+}
+
+(async () => {
+ console.log('Loading active products from mirror…');
+ const r = await pool.query(`
+ SELECT dw_sku, title, vendor, pattern_name
+ FROM shopify_products
+ WHERE status = 'ACTIVE' AND title IS NOT NULL AND handle IS NOT NULL
+ `);
+ const rows = r.rows;
+ console.log('Active products:', rows.length);
+
+ const reports = [
+ summarize('T0 exact title', x => stripVendorSuffix(x.title).toLowerCase(), rows),
+ summarize('T1 normalized title', x => normTitle(x.title), rows),
+ summarize('T2 normalized − colors', x => stripColors(normTitle(x.title)), rows),
+ summarize('PN pattern_name (baseline)',x => (x.pattern_name || '').toLowerCase().trim() || null, rows)
+ ];
+
+ const out = { generated_at: new Date().toISOString(), active_products: rows.length, strategies: reports };
+ const file = path.join(__dirname, 'pattern-grouping-report.json');
+ fs.writeFileSync(file, JSON.stringify(out, null, 2));
+
+ // ---- console digest ----
+ for (const s of reports) {
+ console.log('\n========================================');
+ console.log(s.label);
+ console.log(' distinct groups :', s.distinct_groups);
+ console.log(' multi-colorway groups :', s.multi_colorway_groups);
+ console.log(' products in those :', s.products_in_multi_groups);
+ console.log(' grid rows removed :', (s.products_in_multi_groups - s.multi_colorway_groups), '(' + s.pct_collapsed + ' of catalog collapsed)');
+ console.log(' avg group size :', s.avg_group_size);
+ console.log(' huge groups (>60) :', s.huge_groups_gt60, '(inspect for over-grouping)');
+ console.log(' top 5 largest:');
+ s.top20.slice(0, 5).forEach(g => console.log(` ${g.n}× [${g.vendor}] "${g.key}" e.g. ${JSON.stringify(g.eg[0])}`));
+ }
+ console.log('\nFull report → ', file);
+ await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/tools/pattern-grouping-report.json b/tools/pattern-grouping-report.json
new file mode 100644
index 0000000..2421d75
--- /dev/null
+++ b/tools/pattern-grouping-report.json
@@ -0,0 +1,854 @@
+{
+ "generated_at": "2026-06-26T06:31:26.700Z",
+ "active_products": 74927,
+ "strategies": [
+ {
+ "label": "T0 exact title",
+ "products_keyed": 74873,
+ "distinct_groups": 59710,
+ "multi_colorway_groups": 3388,
+ "products_in_multi_groups": 18551,
+ "pct_collapsed": "20.3%",
+ "avg_group_size": "1.25",
+ "top20": [
+ {
+ "n": 366,
+ "vendor": "Coordonné",
+ "key": "coordonné durable vinyl wallcovering",
+ "eg": [
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+ ]
+ },
+ {
+ "n": 246,
+ "vendor": "Phillipe Romano",
+ "key": "luxuria beige",
+ "eg": [
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Beige | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 216,
+ "vendor": "Surface Stick",
+ "key": "palerma with surface stick - faux wood grain self adhesive",
+ "eg": [
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+ ]
+ },
+ {
+ "n": 149,
+ "vendor": "Roberto Cavalli Wallpaper",
+ "key": "roberto cavalli wallcovering",
+ "eg": [
+ "Roberto Cavalli Wallcovering",
+ "Roberto Cavalli Wallcovering",
+ "Roberto Cavalli Wallcovering"
+ ]
+ },
+ {
+ "n": 138,
+ "vendor": "Surface Stick",
+ "key": "elegante with surface stick - faux fine wood self adhesive",
+ "eg": [
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+ ]
+ },
+ {
+ "n": 112,
+ "vendor": "Surface Stick",
+ "key": "santiago with surface stick - self adhesive",
+ "eg": [
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive"
+ ]
+ },
+ {
+ "n": 98,
+ "vendor": "Versace",
+ "key": "versace wallcovering",
+ "eg": [
+ "Versace Wallcovering",
+ "Versace Wallcovering",
+ "Versace Wallcovering"
+ ]
+ },
+ {
+ "n": 95,
+ "vendor": "Hospitality Wallcoverings",
+ "key": "quick ship durable walls",
+ "eg": [
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls"
+ ]
+ },
+ {
+ "n": 78,
+ "vendor": "Designer Wallcoverings",
+ "key": "ralph mohair velvet from netherlands, holland",
+ "eg": [
+ "Ralph Mohair Velvet from Netherlands, Holland",
+ "Ralph Mohair Velvet from Netherlands, Holland",
+ "Ralph Mohair Velvet from Netherlands, Holland"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Phillipe Romano",
+ "key": "phillipe romano - renaissance metal leaf",
+ "eg": [
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Hollywood Wallcoverings",
+ "key": "ashbourne type ii vinyl",
+ "eg": [
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Designer Wallcoverings",
+ "key": "woodside wood walls",
+ "eg": [
+ "Woodside Wood Walls",
+ "Woodside Wood Walls",
+ "Woodside Wood Walls"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "hollywood abaca grasscloth",
+ "eg": [
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "fine stripes by phillipe romano",
+ "eg": [
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano"
+ ]
+ },
+ {
+ "n": 68,
+ "vendor": "Designer Wallcoverings",
+ "key": "torre wallcovering prints",
+ "eg": [
+ "Torre Wallcovering Prints",
+ "Torre Wallcovering Prints",
+ "Torre Wallcovering Prints"
+ ]
+ },
+ {
+ "n": 62,
+ "vendor": "Koroseal",
+ "key": "wallcovering",
+ "eg": [
+ "Wallcovering | Sarah Rowland | Architectural Wallcoverings",
+ "Wallcovering | Architectural Wallcoverings",
+ "Wallcovering | Sarah Rowland | Architectural Wallcoverings"
+ ]
+ },
+ {
+ "n": 60,
+ "vendor": "Schumacher",
+ "key": "schumacher trim",
+ "eg": [
+ "Schumacher Trim",
+ "Schumacher Trim",
+ "Schumacher Trim"
+ ]
+ },
+ {
+ "n": 55,
+ "vendor": "Phillipe Romano",
+ "key": "luxuria light gray",
+ "eg": [
+ "Luxuria Light Gray | Phillipe Romano",
+ "Luxuria Light Gray | Phillipe Romano",
+ "Luxuria Light Gray | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 54,
+ "vendor": "Phillipe Romano",
+ "key": "grand dame by pr",
+ "eg": [
+ "Grand Dame by Pr",
+ "Grand Dame by Pr",
+ "Grand Dame by Pr"
+ ]
+ },
+ {
+ "n": 54,
+ "vendor": "Newmor Wallcoverings",
+ "key": "cezanne commercial wallcovering",
+ "eg": [
+ "Cezanne Commercial Wallcovering",
+ "Cezanne Commercial Wallcovering",
+ "Cezanne Commercial Wallcovering"
+ ]
+ }
+ ],
+ "huge_groups_gt60": 16
+ },
+ {
+ "label": "T1 normalized title",
+ "products_keyed": 74801,
+ "distinct_groups": 59130,
+ "multi_colorway_groups": 3845,
+ "products_in_multi_groups": 19516,
+ "pct_collapsed": "21.0%",
+ "avg_group_size": "1.27",
+ "top20": [
+ {
+ "n": 374,
+ "vendor": "Coordonné",
+ "key": "coordonn",
+ "eg": [
+ "Coordonné | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+ ]
+ },
+ {
+ "n": 246,
+ "vendor": "Phillipe Romano",
+ "key": "luxuria beige",
+ "eg": [
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Beige | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 216,
+ "vendor": "Surface Stick",
+ "key": "palerma with surface stick faux wood grain",
+ "eg": [
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+ ]
+ },
+ {
+ "n": 149,
+ "vendor": "Roberto Cavalli Wallpaper",
+ "key": "roberto cavalli",
+ "eg": [
+ "Roberto Cavalli Wallcovering",
+ "Roberto Cavalli Wallcovering",
+ "Roberto Cavalli Wallcovering"
+ ]
+ },
+ {
+ "n": 138,
+ "vendor": "Surface Stick",
+ "key": "elegante with surface stick faux fine wood",
+ "eg": [
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+ ]
+ },
+ {
+ "n": 112,
+ "vendor": "Surface Stick",
+ "key": "santiago with surface stick",
+ "eg": [
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive"
+ ]
+ },
+ {
+ "n": 98,
+ "vendor": "Versace",
+ "key": "versace",
+ "eg": [
+ "Versace Wallcovering",
+ "Versace Wallcovering",
+ "Versace Wallcovering"
+ ]
+ },
+ {
+ "n": 95,
+ "vendor": "Hospitality Wallcoverings",
+ "key": "quick ship durable walls",
+ "eg": [
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls"
+ ]
+ },
+ {
+ "n": 78,
+ "vendor": "Designer Wallcoverings",
+ "key": "ralph mohair velvet from netherlands holland",
+ "eg": [
+ "Ralph Mohair Velvet from Netherlands, Holland",
+ "Ralph Mohair Velvet from Netherlands, Holland",
+ "Ralph Mohair Velvet from Netherlands, Holland"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Phillipe Romano",
+ "key": "renaissance metal leaf",
+ "eg": [
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Hollywood Wallcoverings",
+ "key": "ashbourne",
+ "eg": [
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Designer Wallcoverings",
+ "key": "woodside wood walls",
+ "eg": [
+ "Woodside Wood Walls",
+ "Woodside Wood Walls",
+ "Woodside Wood Walls"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "hollywood abaca",
+ "eg": [
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "fine stripes",
+ "eg": [
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano"
+ ]
+ },
+ {
+ "n": 68,
+ "vendor": "Designer Wallcoverings",
+ "key": "torre s",
+ "eg": [
+ "Torre Wallcovering Prints",
+ "Torre Wallcovering Prints",
+ "Torre Wallcovering Prints"
+ ]
+ },
+ {
+ "n": 65,
+ "vendor": "Hollywood Wallcoverings",
+ "key": "faux leaf squares",
+ "eg": [
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares Wallcovering | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 60,
+ "vendor": "Schumacher",
+ "key": "schumacher trim",
+ "eg": [
+ "Schumacher Trim",
+ "Schumacher Trim",
+ "Schumacher Trim"
+ ]
+ },
+ {
+ "n": 55,
+ "vendor": "Phillipe Romano",
+ "key": "luxuria light gray",
+ "eg": [
+ "Luxuria Light Gray | Phillipe Romano",
+ "Luxuria Light Gray | Phillipe Romano",
+ "Luxuria Light Gray | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 54,
+ "vendor": "Phillipe Romano",
+ "key": "grand dame by pr",
+ "eg": [
+ "Grand Dame by Pr",
+ "Grand Dame by Pr",
+ "Grand Dame by Pr"
+ ]
+ },
+ {
+ "n": 54,
+ "vendor": "Newmor Wallcoverings",
+ "key": "cezanne",
+ "eg": [
+ "Cezanne Commercial Wallcovering",
+ "Cezanne Commercial Wallcovering",
+ "Cezanne Commercial Wallcovering"
+ ]
+ }
+ ],
+ "huge_groups_gt60": 16
+ },
+ {
+ "label": "T2 normalized − colors",
+ "products_keyed": 74801,
+ "distinct_groups": 49844,
+ "multi_colorway_groups": 7416,
+ "products_in_multi_groups": 32373,
+ "pct_collapsed": "33.4%",
+ "avg_group_size": "1.50",
+ "top20": [
+ {
+ "n": 594,
+ "vendor": "Phillipe Romano",
+ "key": "luxuria",
+ "eg": [
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Khaki | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 374,
+ "vendor": "Coordonné",
+ "key": "coordonn",
+ "eg": [
+ "Coordonné | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+ ]
+ },
+ {
+ "n": 216,
+ "vendor": "Surface Stick",
+ "key": "palerma with surface stick faux wood grain",
+ "eg": [
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+ ]
+ },
+ {
+ "n": 149,
+ "vendor": "Roberto Cavalli Wallpaper",
+ "key": "roberto cavalli",
+ "eg": [
+ "Roberto Cavalli Wallcovering",
+ "Roberto Cavalli Wallcovering",
+ "Roberto Cavalli Wallcovering"
+ ]
+ },
+ {
+ "n": 143,
+ "vendor": "Kravet",
+ "key": "kravet design",
+ "eg": [
+ "Kravet Design - Beige Commercial Wallcovering | Kravet",
+ "Kravet Design - Beige Wallcovering | Kravet",
+ "Kravet Design - Taupe Natural Wallcovering | Kravet"
+ ]
+ },
+ {
+ "n": 138,
+ "vendor": "Surface Stick",
+ "key": "elegante with surface stick faux fine wood",
+ "eg": [
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+ ]
+ },
+ {
+ "n": 112,
+ "vendor": "Surface Stick",
+ "key": "santiago with surface stick",
+ "eg": [
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive"
+ ]
+ },
+ {
+ "n": 109,
+ "vendor": "Nina Campbell",
+ "key": "nina campbell",
+ "eg": [
+ "Nina Campbell Fabric - Cool Neutral Wallcovering | Nina Campbell",
+ "Nina Campbell Fabric - Peach Beige Wallcovering | Nina Campbell",
+ "Nina Campbell Fabric - Teal Blue Wallcovering | Nina Campbell"
+ ]
+ },
+ {
+ "n": 98,
+ "vendor": "Versace",
+ "key": "versace",
+ "eg": [
+ "Versace Wallcovering",
+ "Versace Wallcovering",
+ "Versace Wallcovering"
+ ]
+ },
+ {
+ "n": 95,
+ "vendor": "Hospitality Wallcoverings",
+ "key": "quick ship durable walls",
+ "eg": [
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls"
+ ]
+ },
+ {
+ "n": 78,
+ "vendor": "Designer Wallcoverings",
+ "key": "ralph mohair velvet from netherlands holland",
+ "eg": [
+ "Ralph Mohair Velvet from Netherlands, Holland",
+ "Ralph Mohair Velvet from Netherlands, Holland",
+ "Ralph Mohair Velvet from Netherlands, Holland"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Phillipe Romano",
+ "key": "renaissance metal leaf",
+ "eg": [
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Hollywood Wallcoverings",
+ "key": "ashbourne",
+ "eg": [
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Designer Wallcoverings",
+ "key": "woodside wood walls",
+ "eg": [
+ "Woodside Wood Walls",
+ "Woodside Wood Walls",
+ "Woodside Wood Walls"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "hollywood abaca",
+ "eg": [
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "fine stripes",
+ "eg": [
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano"
+ ]
+ },
+ {
+ "n": 68,
+ "vendor": "Designer Wallcoverings",
+ "key": "torre s",
+ "eg": [
+ "Torre Wallcovering Prints",
+ "Torre Wallcovering Prints",
+ "Torre Wallcovering Prints"
+ ]
+ },
+ {
+ "n": 67,
+ "vendor": "Daisy Bennett",
+ "key": "daisy bennett",
+ "eg": [
+ "Daisy Bennett - White Wallcovering | Daisy Bennett",
+ "Daisy Bennett - Beige Wallcovering | Daisy Bennett",
+ "Daisy Bennett - White Wallcovering | Daisy Bennett"
+ ]
+ },
+ {
+ "n": 65,
+ "vendor": "Hollywood Wallcoverings",
+ "key": "faux leaf squares",
+ "eg": [
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares | Hollywood Wallcoverings",
+ "Faux Leaf Squares Wallcovering | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 60,
+ "vendor": "Schumacher",
+ "key": "schumacher trim",
+ "eg": [
+ "Schumacher Trim",
+ "Schumacher Trim",
+ "Schumacher Trim"
+ ]
+ }
+ ],
+ "huge_groups_gt60": 19
+ },
+ {
+ "label": "PN pattern_name (baseline)",
+ "products_keyed": 59854,
+ "distinct_groups": 26614,
+ "multi_colorway_groups": 6777,
+ "products_in_multi_groups": 40017,
+ "pct_collapsed": "55.5%",
+ "avg_group_size": "2.25",
+ "top20": [
+ {
+ "n": 1519,
+ "vendor": "Kravet",
+ "key": "kravet design",
+ "eg": [
+ "Kravet Design - W3721-12 Orange Wallcovering | Kravet",
+ "Kravet Design - W4111-11 Slate Wallcovering | Kravet",
+ "Kravet Design - W4154-11 Grey Wallcovering | Kravet"
+ ]
+ },
+ {
+ "n": 660,
+ "vendor": "Phillipe Romano",
+ "key": "luxuria fine textile",
+ "eg": [
+ "Luxuria #d5d8c9 | Phillipe Romano",
+ "Luxuria Beige | Phillipe Romano",
+ "Luxuria Golden Brown | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 366,
+ "vendor": "Coordonné",
+ "key": "coordonné durable vinyl wallcovering",
+ "eg": [
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+ "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+ ]
+ },
+ {
+ "n": 216,
+ "vendor": "Surface Stick",
+ "key": "palerma with surface stick",
+ "eg": [
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+ "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+ ]
+ },
+ {
+ "n": 138,
+ "vendor": "Surface Stick",
+ "key": "elegante with surface stick",
+ "eg": [
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+ "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+ ]
+ },
+ {
+ "n": 115,
+ "vendor": "Novasuede",
+ "key": "novasuede™ fabric & wallcovering",
+ "eg": [
+ "Novasuede™ - Trench Luxury Suede",
+ "Novasuede™ - Taupe Luxury Suede",
+ "Novasuede™ Fabric & Wallcovering - White"
+ ]
+ },
+ {
+ "n": 112,
+ "vendor": "Surface Stick",
+ "key": "santiago with surface stick",
+ "eg": [
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive",
+ "Santiago with Surface Stick - Self Adhesive"
+ ]
+ },
+ {
+ "n": 105,
+ "vendor": "Marburg",
+ "key": "papis loveday wallcovering",
+ "eg": [
+ "Papis Loveday - Beige, Pearl Wallcovering | Marburg",
+ "Papis Loveday Fashion Icon - Gold, Silver Wallcovering | Marburg",
+ "Papis Loveday Fashion Icon - Blue Wallcovering | Marburg"
+ ]
+ },
+ {
+ "n": 101,
+ "vendor": "Clarke And Clarke",
+ "key": "alvar",
+ "eg": [
+ "Alvar - Snow By Clarke And Clarke | Clarke & Clarke Alvar |Solid Texture Multipurpose Velvet",
+ "Alvar - Charcoal By Clarke And Clarke | Clarke & Clarke Alvar |Solid Texture Multipurpose Velvet",
+ "Alvar - Mist By Clarke And Clarke | Clarke & Clarke Alvar |Solid Texture Multipurpose Velvet"
+ ]
+ },
+ {
+ "n": 98,
+ "vendor": "Kravet",
+ "key": "ultrasuede green",
+ "eg": [
+ "Ultrasuede™ Green - Cadet Blue By Kravet Design | Performance |Solid Texture Upholstery Vinyl/Faux Leather / Vegan Leather & Fabric",
+ "Ultrasuede™ Green - Chino Beige By Kravet Design | Performance |Solid Texture Upholstery Vinyl/Faux Leather / Vegan Leather & Fabric",
+ "Ultrasuede™ Green - Flannel Brown By Kravet Design | Performance |Solid Texture Upholstery Vinyl/Faux Leather / Vegan Leather & Fabric"
+ ]
+ },
+ {
+ "n": 89,
+ "vendor": "Brunschwig & Fils",
+ "key": "chevalier wool",
+ "eg": [
+ "Chevalier Wool - Tiger'S Eye Brown By Brunschwig & Fils | |Solid Texture Upholstery Wool",
+ "Chevalier Wool - Saffron Yellow By Brunschwig & Fils | |Solid Texture Upholstery Wool",
+ "Chevalier Wool - Pomegranate Burgundy/Red By Brunschwig & Fils | |Solid Texture Upholstery Wool"
+ ]
+ },
+ {
+ "n": 86,
+ "vendor": "Glitter Walls",
+ "key": "hollywood glamour sequin",
+ "eg": [
+ "Hollywood Glamour Sequin - Lime Iris | Glitter Walls",
+ "Hollywood Glamour Sequin - Sand | Glitter Walls",
+ "Hollywood Glamour Sequin - Bronze | Glitter Walls"
+ ]
+ },
+ {
+ "n": 79,
+ "vendor": "Gaston Y Daniela",
+ "key": "palma",
+ "eg": [
+ "Palma - Verde Billar Green By Gaston Y Daniela | Gaston Maiorica |Solid Upholstery",
+ "Palma - Anil Blue By Gaston Y Daniela | Gaston Maiorica |Solid Upholstery",
+ "Palma - Barquillo Wheat By Gaston Y Daniela | Gaston Maiorica |Solid Upholstery"
+ ]
+ },
+ {
+ "n": 78,
+ "vendor": "Phillipe Romano",
+ "key": "phillipe romano",
+ "eg": [
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano - Renaissance Metal Leaf",
+ "Phillipe Romano Commercial"
+ ]
+ },
+ {
+ "n": 74,
+ "vendor": "Hollywood Wallcoverings",
+ "key": "ashbourne type ii vinyl",
+ "eg": [
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+ "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Designer Wallcoverings",
+ "key": "woodside wood",
+ "eg": [
+ "Woodside Wood Walls",
+ "Woodside Wood Walls",
+ "Woodside Wood Walls"
+ ]
+ },
+ {
+ "n": 73,
+ "vendor": "Phillipe Romano",
+ "key": "hollywood abaca grasscloth",
+ "eg": [
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano",
+ "Hollywood Abaca Grasscloth | Phillipe Romano"
+ ]
+ },
+ {
+ "n": 72,
+ "vendor": "Hospitality Wallcoverings",
+ "key": "quick ship durable",
+ "eg": [
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls",
+ "Quick Ship Durable Walls"
+ ]
+ },
+ {
+ "n": 72,
+ "vendor": "Phillipe Romano",
+ "key": "fine stripes",
+ "eg": [
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano",
+ "Fine Stripes by Phillipe Romano"
+ ]
+ },
+ {
+ "n": 69,
+ "vendor": "Clarke And Clarke",
+ "key": "highlander",
+ "eg": [
+ "Highlander - Cinnamon By Clarke And Clarke | Clarke & Clarke Highlander 2 |Solid Texture Multipurpose",
+ "Highlander - Antique By Clarke And Clarke | Clarke & Clarke Highlander 2 |Solid Texture Multipurpose",
+ "Highlander - Berry By Clarke And Clarke | Clarke & Clarke Highlander |Solid Texture Multipurpose"
+ ]
+ }
+ ],
+ "huge_groups_gt60": 24
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tools/pattern-image-verify.py b/tools/pattern-image-verify.py
new file mode 100644
index 0000000..8f843cb
--- /dev/null
+++ b/tools/pattern-image-verify.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""WS-1 image confirmation (READ-ONLY, local, $0).
+
+Reads "dw_sku<TAB>title<TAB>image_url" lines on stdin (one candidate group),
+fetches each image, computes a COLOR-INVARIANT structural signature, and clusters
+members by visual pattern. Same pattern in different colorways -> one cluster;
+genuinely different patterns sharing a key (e.g. a templated-title vendor) ->
+multiple clusters. This is the "are these really the same pattern, just different
+colors?" confirmation on top of the SKU/title key.
+
+Color invariance: image -> grayscale -> we compute dHash on BOTH the image and its
+tonal inverse and take the smaller Hamming distance, so a light-on-dark vs
+dark-on-light colorway of the same motif still matches.
+
+ psql ... -tAc "<query yielding sku|title|url>" | tr '|' '\\t' | \
+ python3 tools/pattern-image-verify.py "Coordonné" 30
+"""
+import sys, io, urllib.request, hashlib, os
+from PIL import Image, ImageOps
+
+LABEL = sys.argv[1] if len(sys.argv) > 1 else "group"
+LIMIT = int(sys.argv[2]) if len(sys.argv) > 2 else 40
+THRESH = int(os.environ.get("HAM_THRESH", "12")) # dHash Hamming <= THRESH => same pattern
+HASH = 8
+CACHE = "/tmp/pattern-verify-cache"
+os.makedirs(CACHE, exist_ok=True)
+
+def dhash(img, n=HASH):
+ g = img.convert("L").resize((n + 1, n), Image.LANCZOS)
+ px = list(g.getdata())
+ v = 0
+ for r in range(n):
+ base = r * (n + 1)
+ for c in range(n):
+ v = (v << 1) | (1 if px[base + c] > px[base + c + 1] else 0)
+ return v
+
+def inv_dhash(img, n=HASH):
+ return dhash(ImageOps.invert(img.convert("L")), n)
+
+def ham(a, b):
+ return bin(a ^ b).count("1")
+
+def fetch(url):
+ key = os.path.join(CACHE, hashlib.md5(url.encode()).hexdigest() + ".bin")
+ if os.path.exists(key):
+ with open(key, "rb") as f:
+ return f.read()
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 dw-pattern-verify"})
+ data = urllib.request.urlopen(req, timeout=20).read()
+ with open(key, "wb") as f:
+ f.write(data)
+ return data
+
+items = []
+for line in sys.stdin:
+ parts = line.rstrip("\n").split("\t")
+ if len(parts) < 3:
+ continue
+ sku, title, url = parts[0], parts[1], parts[2]
+ if not url.startswith("http"):
+ continue
+ items.append((sku, title, url))
+ if len(items) >= LIMIT:
+ break
+
+print(f"=== {LABEL}: {len(items)} candidate members (thresh dHash<={THRESH}) ===")
+hashed = []
+for sku, title, url in items:
+ try:
+ im = Image.open(io.BytesIO(fetch(url)))
+ hashed.append((sku, title, dhash(im), inv_dhash(im)))
+ except Exception as e:
+ print(f" [skip] {sku}: {e}")
+
+# single-linkage clustering: a member joins a cluster if it's within THRESH of ANY member
+clusters = []
+for sku, title, h, hi in hashed:
+ placed = False
+ for cl in clusters:
+ for (_, _, ch, _chi) in cl:
+ if min(ham(h, ch), ham(hi, ch)) <= THRESH:
+ cl.append((sku, title, h, hi))
+ placed = True
+ break
+ if placed:
+ break
+ if not placed:
+ clusters.append([(sku, title, h, hi)])
+
+clusters.sort(key=len, reverse=True)
+print(f" -> {len(hashed)} images formed {len(clusters)} distinct visual pattern(s)")
+for i, cl in enumerate(clusters[:8], 1):
+ egs = "; ".join(t for (_, t, _, _) in cl[:3])
+ print(f" cluster {i}: {len(cl):>3} member(s) e.g. {egs[:90]}")
+if len(clusters) > 8:
+ print(f" … +{len(clusters)-8} more clusters")
+
+# verdict for this candidate group
+if hashed:
+ biggest = len(clusters[0]) / len(hashed)
+ if len(clusters) == 1:
+ print(" VERDICT: CONFIRMED single pattern — safe to group as colorways.")
+ elif biggest >= 0.6:
+ print(f" VERDICT: MOSTLY one pattern ({len(clusters[0])}/{len(hashed)}) + {len(clusters)-1} outlier(s) to split off.")
+ else:
+ print(f" VERDICT: OVER-GROUPED — {len(clusters)} real patterns share this key. Do NOT group by key alone.")
← 5e32b37 Add /api/similar (visually-similar designs) + similar-design
·
back to Dw Pairs Well
·
Add pattern-id-backfill dry-run (Option-A hybrid) + texture- 90dbcaf →