← back to Dw Marketing Reels
novasuede color post (TK-10866): drop designerwallcoverings from rotation + swap jewelry creative for SKU-labeled product swatch
99c71a897351865f2e23821c815239c18ced0c4a · 2026-08-26 08:28:01 -0700 · Steve Abrams
- Rotation limited to suedewallpaper,textilewallpaper,commercialwallcoverings; hard guard strips designerwallcoverings even if re-added via NS_ACCOUNTS
- Creative now uses the product swatch (images/colors) with the real DWNS-* SKU overlaid + in the caption; jewelry-counter photo removed from the reel
- SKU map (data/novasuede-sku-map.json) joined from dw_unified novasuede_catalog by name-slug; 142/142 verified, zero fabrication
- caption + header comment de-jeweled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M scripts/novasuede-run-all.mjs
Diff
commit 99c71a897351865f2e23821c815239c18ced0c4a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 26 08:28:01 2026 -0700
novasuede color post (TK-10866): drop designerwallcoverings from rotation + swap jewelry creative for SKU-labeled product swatch
- Rotation limited to suedewallpaper,textilewallpaper,commercialwallcoverings; hard guard strips designerwallcoverings even if re-added via NS_ACCOUNTS
- Creative now uses the product swatch (images/colors) with the real DWNS-* SKU overlaid + in the caption; jewelry-counter photo removed from the reel
- SKU map (data/novasuede-sku-map.json) joined from dw_unified novasuede_catalog by name-slug; 142/142 verified, zero fabrication
- caption + header comment de-jeweled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/novasuede-run-all.mjs | 36 ++++++++++++++++++++++++------------
1 file changed, 24 insertions(+), 12 deletions(-)
diff --git a/scripts/novasuede-run-all.mjs b/scripts/novasuede-run-all.mjs
index 36610cf..84348c2 100644
--- a/scripts/novasuede-run-all.mjs
+++ b/scripts/novasuede-run-all.mjs
@@ -1,7 +1,8 @@
#!/usr/bin/env node
// novasuede-run-all.mjs — one "drop" of the 3x/week campaign: for EACH target account, pick 3
-// NEW colors (global de-dup), build a 15s post (jewelry-display + swatch per color + music bed,
-// first frame = jewelry display), push to Kamatera, and post to that account. Marks colors posted.
+// NEW colors (global de-dup), build a 15s post (product SWATCH per color with the real DWNS SKU
+// overlaid + music bed; TK-10866: NO jewelry/room-setting photo), push to Kamatera, and post to
+// that account. Marks colors posted. Rotation = suede/textile/commercial ONLY (never designerwallcoverings).
// $0 render. Live posting is the armed step (SOCIAL_LIVE_ARMED / NS_POST=1).
import { writeFileSync, readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'node:fs';
import { execSync, execFileSync } from 'node:child_process';
@@ -15,8 +16,11 @@ const MUSIC = process.env.NS_MUSIC || join(WS, 'audio', 'jazz.wav');
const STATE = join(ROOT, 'data', 'novasuede-post-state.json');
// NOTE: @novasuede is NOT Steve's IG handle — deliberately excluded. Novasuede is the product/site;
// the content posts to Steve's own suede/textile/commercial/DW accounts.
-const ACCOUNTS = (process.env.NS_ACCOUNTS || 'suedewallpaper,textilewallpaper,commercialwallcoverings,designerwallcoverings')
- .split(',').map(s => s.trim().replace(/^@/, '')).filter(Boolean);
+// TK-10866: Novasuede posts NEVER go to @designerwallcoverings — rotation is limited to the three
+// sister accounts. A hard guard below strips designerwallcoverings even if it re-enters via NS_ACCOUNTS.
+const ACCOUNTS = (process.env.NS_ACCOUNTS || 'suedewallpaper,textilewallpaper,commercialwallcoverings')
+ .split(',').map(s => s.trim().replace(/^@/, '')).filter(Boolean)
+ .filter(a => a.toLowerCase() !== 'designerwallcoverings');
const PER_ACCT = Number(process.env.NS_COLORS || 3);
const POST = process.env.NS_POST === '1';
const AUTH = process.env.IG_AGENT_AUTH || '';
@@ -26,13 +30,18 @@ const PUSH_HOST = process.env.PUSH_HOST || 'root@45.61.58.125';
const PUSH_PATH = process.env.PUSH_PATH || '/root/public-projects/dw-marketing-reels';
const colors = (() => { const d = JSON.parse(readFileSync(join(NS, 'colors-data.json'), 'utf8')); return Array.isArray(d) ? d : (d.colors || []); })();
-const hasJewel = s => existsSync(join(NS, 'images', 'jewelry', `novasuede-${s}.jpg`));
+// Authoritative slug -> real DW SKU map, generated from the live dw_unified novasuede_catalog
+// (dw_sku column). NEVER invent a SKU: a color is only eligible if it has a real DWNS-* here.
+const SKU_MAP = (() => { const p = join(ROOT, 'data', 'novasuede-sku-map.json'); return existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {}; })();
+const skuFor = s => { const v = SKU_MAP[s]; return (typeof v === 'string' && /^DWNS-\d+$/.test(v)) ? v : null; };
const hasSwatch = s => existsSync(join(NS, 'images', 'colors', `novasuede-${s}.jpg`));
const esc = s => String(s || '').replace(/[&<>"]/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[m]));
const tc = s => s.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
const state = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : { posted: [] };
const done = new Set(state.posted || []);
-const eligible = colors.filter(c => !done.has(c.slug) && hasJewel(c.slug) && hasSwatch(c.slug));
+// NEW (TK-10866): show the SKU on the product SWATCH, not the jewelry-counter styled shot.
+// Eligibility = swatch image present AND a real DWNS-* SKU exists for the slug.
+const eligible = colors.filter(c => !done.has(c.slug) && hasSwatch(c.slug) && skuFor(c.slug));
let ei = 0;
const takeColors = n => { const out = []; while (out.length < n && ei < eligible.length) out.push(eligible[ei++]); return out; };
@@ -40,15 +49,17 @@ function findMp4(d) { let n=null,mt=0; const w=x=>{for(const e of readdirSync(x,
function buildReel(cols, tag) {
const A = join(WS, 'assets', 'nsr'); mkdirSync(A, { recursive: true });
- cols.forEach((c, i) => { copyFileSync(join(NS, 'images', 'jewelry', `novasuede-${c.slug}.jpg`), join(A, `j${i}.jpg`)); copyFileSync(join(NS, 'images', 'colors', `novasuede-${c.slug}.jpg`), join(A, `s${i}.jpg`)); });
+ // Creative now uses the product SWATCH (images/colors) — the SKU is overlaid on it. Jewelry
+ // shots are no longer built into the reel (TK-10866).
+ cols.forEach((c, i) => { copyFileSync(join(NS, 'images', 'colors', `novasuede-${c.slug}.jpg`), join(A, `s${i}.jpg`)); });
const TOTAL = 15, INTRO = 3, OUTRO = 3;
const PER = +(((TOTAL - INTRO - OUTRO) / cols.length)).toFixed(3);
- // scenes: intro (jewelry of color0) then per color: jewelry card
- const items = [{ img: `nsr/j0.jpg`, kick: 'Novasuede™ · New Colours', big: `${cols.length} New`, sub: 'Microfiber suede' },
- ...cols.map((c, i) => ({ img: `nsr/j${i}.jpg`, kick: 'New Colour', big: esc(tc(c.slug)), sub: `Soft · durable · ${esc(c.hex)}` }))];
+ // scenes: intro (swatch of color0) then per color: swatch card with the real SKU overlaid
+ const items = [{ img: `nsr/s0.jpg`, kick: 'Novasuede™ · New Colours', big: `${cols.length} New`, sub: 'Microfiber suede' },
+ ...cols.map((c, i) => ({ img: `nsr/s${i}.jpg`, kick: 'New Colour', big: esc(tc(c.slug)), sub: `SKU ${esc(skuFor(c.slug))} · ${esc(c.hex)}` }))];
const starts = [0]; let t = INTRO; for (let i = 0; i < cols.length; i++) { starts.push(t); t += PER; }
const durs = [INTRO, ...cols.map(() => PER)];
- const scenes = items.map((s, i) => `<div class="scene" id="s${i}"><img class="ph" src="assets/${s.img}" alt="Novasuede ${esc(s.big)} jewelry display"/><div class="scrim"></div><div class="cap"><div class="kick">${s.kick}</div><div class="big">${s.big}</div><div class="sub">${s.sub}</div></div></div>`).join('');
+ const scenes = items.map((s, i) => `<div class="scene" id="s${i}"><img class="ph" src="assets/${s.img}" alt="Novasuede ${esc(s.big)} colour swatch"/><div class="scrim"></div><div class="cap"><div class="kick">${s.kick}</div><div class="big">${s.big}</div><div class="sub">${s.sub}</div></div></div>`).join('');
const anim = items.map((s, i) => `gsap.set("#s${i}",{opacity:${i===0?1:0}});gsap.set("#s${i} .ph",{scale:1.1});gsap.set("#s${i} .cap",{y:40,opacity:0});${i===0?'':`tl.to("#s${i}",{opacity:1,duration:0.4},${starts[i]});`}tl.to("#s${i} .ph",{scale:1,duration:${durs[i]},ease:"none"},${starts[i]});tl.to("#s${i} .cap",{y:0,opacity:1,duration:0.5,ease:"power2.out"},${(starts[i]+0.25).toFixed(2)});tl.to("#s${i}",{opacity:0,duration:0.4,ease:"power1.in"},${(starts[i]+durs[i]-0.4).toFixed(2)});`).join('\n');
const OUTRO_AT = INTRO + cols.length * PER;
const html = `<!doctype html><html data-resolution="portrait"><head><meta charset="UTF-8"/><script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script><style>
@@ -82,7 +93,8 @@ ${anim}
async function postTo(account, file, cols) {
const url = `${BASE}/reels/${encodeURIComponent(file)}`;
const names = cols.map(c => tc(c.slug)).join(', ');
- const cap = `Novasuede™ New Colours: ${names} — luxury microfiber suede, jewelry-case ready. Samples are always no charge for design professionals. novasuede.com\n\n#Novasuede #MicrofiberSuede #Suede #InteriorDesign #LuxuryInteriors #DesignTrade #FabricOfTheDay`;
+ const skus = cols.map(c => skuFor(c.slug)).filter(Boolean).join(', ');
+ const cap = `Novasuede™ New Colours: ${names}${skus ? ` (${skus})` : ''} — luxury microfiber suede. Samples are always no charge for design professionals. novasuede.com\n\n#Novasuede #MicrofiberSuede #Suede #InteriorDesign #LuxuryInteriors #DesignTrade #FabricOfTheDay`;
const ac = new AbortController(); const t = setTimeout(() => ac.abort(), 240000);
try {
const res = await fetch(`${IG}/api/skill/reel`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: AUTH }, body: JSON.stringify({ video_url: url, caption: cap, account, source: 'novasuede colours 3x/week' }), signal: ac.signal });
← 5e4c954 auto-data-snapshot: 2026-08-26T08:21:31 (2 data files) — dat
·
back to Dw Marketing Reels
·
Reel Studio: render a single photo + song → mp4 (Composer ha 9de1fc9 →