← back to Dw Marketing Reels
scripts/suggest-videos.mjs
121 lines
#!/usr/bin/env node
// suggest-videos.mjs — the "hypergen" daily idea engine for the DW Marketing console.
//
// Every day this proposes FOUR video ideas (the "4 suggested videos a day") built from
// the live New Arrivals catalog (data/new-arrivals.json). Each idea maps 1:1 onto a real
// HyperFrames render job the Video Studio already knows how to run (reels-producer /
// product-launch-video), so "Use it" in the console just queues that gated job.
//
// HARD RULES honoured here:
// • $0 local — pure heuristics over the catalog, no paid LLM/API call.
// • Keep ALL history — suggestions are append-only, grouped by day; nothing is deleted.
// • Idempotent per day — a given local (PT) date is only ever filled to 4 once; re-runs
// top up a short day but never duplicate or clobber a suggestion already USED.
//
// Usage: node scripts/suggest-videos.mjs [--force]
// --force replace today's UNUSED suggestions and regenerate (used ones are preserved).
import { readFile, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const NA_FILE = join(ROOT, 'data', 'new-arrivals.json');
const OUT_FILE = join(ROOT, 'data', 'video-suggestions.json');
const NA_URL = 'https://designerwallcoverings.com/collections/new-arrivals';
const FORCE = process.argv.includes('--force');
// Local (America/Los_Angeles) calendar date, e.g. "2026-07-22" — the day this drop belongs to.
function ptDate(d = new Date()) {
return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Los_Angeles' }).format(d);
}
async function readJson(fp, fallback) {
return existsSync(fp) ? JSON.parse(await readFile(fp, 'utf8')) : fallback;
}
// Stable-ish id without Math.random (harness-friendly): date + slot + short hash of the seed.
function makeId(date, slot, seed) {
let h = 0; for (const c of String(seed)) h = (h * 31 + c.charCodeAt(0)) >>> 0;
return `sg-${date}-${slot}-${h.toString(36)}`;
}
const ref = it => ({ pattern: it.pattern || it.title || 'Untitled', image: it.image_hi || it.image || '', url: it.url || '' });
// Build the four ideas for `date` from the catalog `items`.
function buildIdeas(date, items, createdISO) {
const pick = (arr, n) => arr.slice(0, n);
// vendor with the most fresh arrivals → the "spotlight" of the day
const byVendor = {};
for (const it of items) { const v = it.vendor || 'Assorted'; (byVendor[v] ||= []).push(it); }
const topVendor = Object.entries(byVendor).sort((a, b) => b[1].length - a[1].length)[0] || ['Assorted', items];
// highest-priced fresh arrival → the "statement piece"
const priced = items.filter(it => Number(it.price) > 0).sort((a, b) => Number(b.price) - Number(a.price));
const statement = priced[0] || items[0] || null;
const ideas = [];
const push = (slot, o) => ideas.push({
id: makeId(date, slot, o.seed), date, slot, created_at: createdISO,
kind: o.kind, format: o.format, url: o.url || '',
title: o.title, idea: o.idea, hero_image: o.hero || '', refs: (o.refs || []).map(ref),
used: false, used_at: null, used_where: null,
});
// 1 — the flagship weekly drop reel (9:16)
if (items.length) push(1, {
seed: 'drop:' + date, kind: 'new-arrivals-reel', format: '9:16',
title: `New Arrivals reel — the ${date} drop`,
idea: `A 9:16 Instagram reel of this week's freshest 6–8 arrivals, varied by color & scale. Lead with the most striking image, jewel-tone first. Ready-to-post DW caption.`,
hero: (items[0].image_hi || items[0].image), refs: pick(items, 6),
});
// 2 — vendor spotlight, square feed grid (1:1)
if (topVendor[1].length) push(2, {
seed: 'vendor:' + topVendor[0] + ':' + date, kind: 'new-arrivals-reel', format: '1:1',
title: `Feed grid — ${topVendor[0]} spotlight`,
idea: `A 1:1 feed post spotlighting ${topVendor[0]} (${topVendor[1].length} new this drop). Tight 4–6 product montage, brand-forward. Caption names the house.`,
hero: (topVendor[1][0].image_hi || topVendor[1][0].image), refs: pick(topVendor[1], 6),
});
// 3 — collection tour straight off the live storefront URL (9:16)
push(3, {
seed: 'tour:' + date, kind: 'url-reel', format: '9:16', url: NA_URL,
title: 'Collection tour — New Arrivals page',
idea: `A 9:16 promo tour built from the live New Arrivals collection page (${NA_URL}). Cinematic scroll of the whole drop, DW wordmark open/close.`,
hero: (items[0] && (items[0].image_hi || items[0].image)) || '', refs: pick(items, 4),
});
// 4 — statement-piece spotlight from its own product URL (9:16)
if (statement) push(4, {
seed: 'statement:' + (statement.url || statement.pattern) + ':' + date, kind: 'url-reel', format: '9:16', url: statement.url,
title: `Statement piece — ${statement.pattern}`,
idea: `A 9:16 hero spotlight of "${statement.pattern}"${statement.vendor ? ' by ' + statement.vendor : ''}${Number(statement.price) ? ` ($${statement.price})` : ''} — the drop's showpiece. Slow push-in on the mural, one-line hook.`,
hero: (statement.image_hi || statement.image), refs: [statement],
});
return ideas;
}
(async () => {
const na = await readJson(NA_FILE, { items: [] });
const items = na.items || [];
if (!items.length) { console.log('suggest-videos: no New Arrivals in the feed — nothing to suggest.'); return; }
const all = await readJson(OUT_FILE, []);
const date = ptDate();
const createdISO = new Date().toISOString();
const todays = all.filter(s => s.date === date);
if (FORCE) {
// drop today's UNUSED ideas, keep anything already used, then rebuild
const keep = all.filter(s => s.date !== date || s.used);
const fresh = buildIdeas(date, items, createdISO).filter(n => !keep.some(k => k.slot === n.slot && k.date === date));
const next = [...keep, ...fresh];
await writeFile(OUT_FILE, JSON.stringify(next, null, 2));
console.log(`suggest-videos: [force] ${date} → ${fresh.length} regenerated, ${next.length} total kept.`);
return;
}
if (todays.length >= 4) { console.log(`suggest-videos: ${date} already has ${todays.length} suggestions — no-op.`); return; }
// top up today's slots that don't exist yet
const have = new Set(todays.map(s => s.slot));
const add = buildIdeas(date, items, createdISO).filter(n => !have.has(n.slot));
if (!add.length) { console.log(`suggest-videos: ${date} nothing to add.`); return; }
const next = [...all, ...add];
await writeFile(OUT_FILE, JSON.stringify(next, null, 2));
console.log(`suggest-videos: ${date} +${add.length} suggestion(s) → ${next.length} total (all history retained).`);
})();