[object Object]

← back to Marketing Command Center

new-arrivals-reel: distinct-colorway heroes (URL-keyed cache + byte-distinct assert) + SKU lower-third

5999977ac287253ce644b82ed1598af46d066ba7 · 2026-09-01 08:03:44 -0700 · Steve Abrams

Fixes the two v1 bugs: (1) index-keyed image cache reused shared/wrong-colorway
heroes — now reads correct_image_url, keys the cache by URL hash, and asserts
all 10 heroes are byte-distinct before rendering; (2) adds a SKU lower-third
line (DW/Shopify SKU primary in cream + Sanderson article secondary). Reads
/tmp/na10-verified.json. $0 local (ffmpeg + ImageMagick).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 5999977ac287253ce644b82ed1598af46d066ba7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 08:03:44 2026 -0700

    new-arrivals-reel: distinct-colorway heroes (URL-keyed cache + byte-distinct assert) + SKU lower-third
    
    Fixes the two v1 bugs: (1) index-keyed image cache reused shared/wrong-colorway
    heroes — now reads correct_image_url, keys the cache by URL hash, and asserts
    all 10 heroes are byte-distinct before rendering; (2) adds a SKU lower-third
    line (DW/Shopify SKU primary in cream + Sanderson article secondary). Reads
    /tmp/na10-verified.json. $0 local (ffmpeg + ImageMagick).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/new-arrivals-reel.mjs | 93 +++++++++++++++++++++++++++++++++++--------
 1 file changed, 77 insertions(+), 16 deletions(-)

diff --git a/scripts/new-arrivals-reel.mjs b/scripts/new-arrivals-reel.mjs
index 69df093..019110b 100644
--- a/scripts/new-arrivals-reel.mjs
+++ b/scripts/new-arrivals-reel.mjs
@@ -22,6 +22,7 @@ import { execFileSync } from 'node:child_process';
 import fs from 'node:fs';
 import path from 'node:path';
 import os from 'node:os';
+import crypto from 'node:crypto';
 
 // ---- args -------------------------------------------------------------
 const args = Object.fromEntries(
@@ -69,47 +70,107 @@ function parseTitle(raw) {
 
 function esc(s) { return s.replace(/'/g, "’").replace(/:/g, '꞉'); }
 
+// Resolve the colorway-accurate hero URL for a product. The verified source
+// data (na10-verified.json) carries `correct_image_url` — a byte-distinct
+// Sanderson CDN JPEG per colorway. Fall back to legacy `image`/`image_url`
+// only if the verified field is absent.
+function heroUrl(p) {
+  return p.correct_image_url || p.image_url || p.image || p.hero || '';
+}
+// DW/Shopify storefront SKU (primary) + Sanderson article code (secondary).
+function skuLines(p) {
+  const dw = p.shopify_sku || p.sku || '';
+  const mfr = p.sanderson_sku_authoritative || p.mfr_sku || '';
+  return { dw, mfr };
+}
+
 // ---- load data --------------------------------------------------------
 const products = JSON.parse(fs.readFileSync(DATA, 'utf8'));
 if (!Array.isArray(products) || !products.length) throw new Error('no products in ' + DATA);
 [PREP, WORK].forEach((d) => fs.mkdirSync(d, { recursive: true }));
 fs.mkdirSync(SRC, { recursive: true });
 
-// ensure images present (download if missing)
-products.forEach((p, i) => {
-  const f = path.join(SRC, `img-${String(i + 1).padStart(2, '0')}.jpg`);
-  if (!fs.existsSync(f)) {
-    execFileSync('curl', ['-sSL', '-o', f, p.image]);
+// ensure images present. IMPORTANT (bug-fix): the FIRST cut reused shared /
+// wrong-colorway heroes because the cache was keyed by index (img-01.jpg …)
+// and a stale wrong file short-circuited the download. We now key each cached
+// file by a hash of its resolved URL, so a changed colorway URL always
+// re-fetches, and index-01 can never accidentally serve a prior batch's bytes.
+const beatSrc = products.map((p, i) => {
+  const url = heroUrl(p);
+  if (!url) throw new Error(`no image url for beat ${i + 1} (${p.title})`);
+  const key = crypto.createHash('sha1').update(url).digest('hex').slice(0, 12);
+  const f = path.join(SRC, `img-${String(i + 1).padStart(2, '0')}-${key}.jpg`);
+  if (!fs.existsSync(f) || fs.statSync(f).size < 1024) {
+    execFileSync('curl', ['-sSL', '--fail', '-o', f, url]);
   }
+  return f;
 });
 
+// Assert every colorway hero is BYTE-distinct (the exact defect that shipped
+// the first cut: img-02/03/04 all 55656 bytes, img-08/09/10 all 114734).
+{
+  const seen = new Map();
+  beatSrc.forEach((f, i) => {
+    const sum = crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
+    if (seen.has(sum)) {
+      throw new Error(
+        `DUPLICATE hero: beat ${i + 1} (${products[i].title}) is byte-identical to ` +
+        `beat ${seen.get(sum) + 1} (${products[seen.get(sum)].title}). ` +
+        `correct_image_url did not resolve to a distinct colorway image.`
+      );
+    }
+    seen.set(sum, i);
+  });
+  process.stdout.write(`  ✓ ${beatSrc.length} colorway heroes verified byte-distinct\n`);
+}
+
 // ---- build the DW wordmark strip (reused on every lower-third) ---------
 // "DESIGNER WALLCOVERINGS" — spaced small-caps sans, hairline rule above.
-function drawLowerThird(basePanel, name, colorway, vendor) {
+function drawLowerThird(basePanel, name, colorway, vendor, sku = {}) {
   // Build a standalone transparent lower-third layer (scrim + text)...
   const lt = path.join(WORK, `lt-${path.basename(basePanel, '.png')}.png`);
-  magick([
+  // SKU lines: primary = DW/Shopify storefront SKU (small caps, muted);
+  // secondary = Sanderson article code (lighter, one tidy line). Only render
+  // what exists so we never leave a stray "SKU " with no value.
+  const skuMain = sku.dw ? `SKU ${sku.dw}` : '';
+  const skuSecondary = sku.mfr ? `Sanderson ${sku.mfr}` : '';
+  const a = [
     '-size', `${W}x${H}`, 'xc:none',
-    // lower gradient scrim for legibility
-    '(', '-size', `${W}x560`, 'gradient:none-rgba(20,18,15,0.66)', ')',
+    // lower gradient scrim for legibility (slightly taller to seat the SKU line)
+    '(', '-size', `${W}x600`, 'gradient:none-rgba(18,16,13,0.74)', ')',
     '-gravity', 'south', '-composite',
     '-gravity', 'south',
     // "New Arrival" eyebrow
     '-font', SANS, '-pointsize', '30', '-fill', ACCENT,
-    '-kerning', '8', '-annotate', '+0+300', 'NEW ARRIVAL',
+    '-kerning', '8', '-annotate', '+0+320', 'NEW ARRIVAL',
     // pattern name (display serif)
     '-font', SERIF, '-pointsize', '76', '-fill', '#FBF7EE',
-    '-kerning', '1', '-annotate', `+0+200`, esc(name),
+    '-kerning', '1', '-annotate', `+0+220`, esc(name),
     // colorway (serif)
     '-font', SERIF_G, '-pointsize', '38', '-fill', '#E6DCC8',
-    '-annotate', '+0+150', esc(colorway || vendor),
+    '-annotate', '+0+170', esc(colorway || vendor),
+  ];
+  // SKU line — small-caps spec label, wide-kerned. Warm cream (not the muted
+  // brass) so the storefront identifier stays legible over EVERY swatch tone,
+  // including the pale ivory grounds where brass-on-scrim washes out.
+  if (skuMain) {
+    a.push('-font', SANS, '-pointsize', '27', '-fill', '#EBDFC6',
+      '-kerning', '4', '-annotate', '+0+120', esc(skuMain));
+  }
+  // Sanderson article — lighter secondary, tucked just under the DW SKU.
+  if (skuSecondary) {
+    a.push('-font', SANS, '-pointsize', '21', '-fill', '#C8BB9E',
+      '-kerning', '2', '-annotate', '+0+88', esc(skuSecondary));
+  }
+  a.push(
     // hairline rule
     '-fill', RULE, '-draw', `rectangle 390,${H - 118} 690,${H - 116}`,
     // DW wordmark
     '-font', SANS, '-pointsize', '26', '-fill', '#D8CDB4',
     '-kerning', '6', '-annotate', '+0+64', 'DESIGNER WALLCOVERINGS',
     lt,
-  ]);
+  );
+  magick(a);
   // ...then composite it ONTO the base swatch panel (in place).
   magick([basePanel, lt, '-gravity', 'center', '-composite', basePanel]);
 }
@@ -117,7 +178,7 @@ function drawLowerThird(basePanel, name, colorway, vendor) {
 // ---- render one product beat panel ------------------------------------
 // Framed swatch on ivory: swatch inset with a hairline keyline, generous margins.
 function renderBeat(i, p) {
-  const src = path.join(SRC, `img-${String(i + 1).padStart(2, '0')}.jpg`);
+  const src = beatSrc[i];   // colorway-accurate, byte-verified hero (see above)
   const panel = path.join(PREP, `beat-${String(i + 1).padStart(2, '0')}.png`);
   const { name, colorway } = splitName(parseTitle(p.title));
   const swatch = 812;        // swatch size on the ivory ground
@@ -138,8 +199,8 @@ function renderBeat(i, p) {
     '-gravity', 'north', '-geometry', `+0+${top}`, '-composite',
     panel,
   ]);
-  // 2) lower-third overlay
-  drawLowerThird(panel, name, colorway, p.vendor);
+  // 2) lower-third overlay (now carries the SKU line)
+  drawLowerThird(panel, name, colorway, p.vendor, skuLines(p));
   return panel;
 }
 

← b7ffb5f auto-data-snapshot: 2026-09-01T07:51:05 (1 data files) — dat  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-09-01T09:05:22 (1 data files) — pub 5c99403 →