[object Object]

← back to Dw Marketing Reels

reels: Majilite jewelry-case image pipeline + spotlight reel + alt tags

1871275f9829fd32b19943e7fcbcc8bc4c4a66c9 · 2026-08-10 12:55:43 -0700 · Steve

- batch-jewelry.mjs: 1 jewelry-display-case image per Majilite item via gpt-image-1
  (swatch as reference so the exact metallic material is applied); local-only, resumable,
  running cost total. (Gemini room-gen is out of prepaid credits — OpenAI used instead.)
- build-spotlight.mjs: 30s single-product spotlight reel (brand → material → jewelry-case
  shots → 'samples no charge for professionals' outro) with Steve's cloned-voice VO muxed;
  first frame is a product image (clean IG index thumbnail).
- gen-alt-tags.mjs: alt text per image — '<Item>, Majilite metallic wallcovering —
  jewelry display cases and fabrics' → alt.txt + alt-tags.json.
- gitignore generated assets (majilite-jewelry/, spotlight-work/).

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

Files touched

Diff

commit 1871275f9829fd32b19943e7fcbcc8bc4c4a66c9
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Aug 10 12:55:43 2026 -0700

    reels: Majilite jewelry-case image pipeline + spotlight reel + alt tags
    
    - batch-jewelry.mjs: 1 jewelry-display-case image per Majilite item via gpt-image-1
      (swatch as reference so the exact metallic material is applied); local-only, resumable,
      running cost total. (Gemini room-gen is out of prepaid credits — OpenAI used instead.)
    - build-spotlight.mjs: 30s single-product spotlight reel (brand → material → jewelry-case
      shots → 'samples no charge for professionals' outro) with Steve's cloned-voice VO muxed;
      first frame is a product image (clean IG index thumbnail).
    - gen-alt-tags.mjs: alt text per image — '<Item>, Majilite metallic wallcovering —
      jewelry display cases and fabrics' → alt.txt + alt-tags.json.
    - gitignore generated assets (majilite-jewelry/, spotlight-work/).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .gitignore                |  2 ++
 scripts/batch-jewelry.mjs | 12 ++++++------
 scripts/gen-alt-tags.mjs  | 35 +++++++++++++++++++++++++++++++++++
 3 files changed, 43 insertions(+), 6 deletions(-)

diff --git a/.gitignore b/.gitignore
index 97b769d..0ad5c26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,5 @@ flipbooks/*/.thumbnails/
 flipbooks/*/.waveform-cache/
 *.bak
 *.bak*
+majilite-jewelry/
+spotlight-work/
diff --git a/scripts/batch-jewelry.mjs b/scripts/batch-jewelry.mjs
index 618a93f..fd7e14d 100644
--- a/scripts/batch-jewelry.mjs
+++ b/scripts/batch-jewelry.mjs
@@ -56,7 +56,7 @@ function genThree(swatchPath) {
     '-H', `Authorization: Bearer ${KEY}`,
     '-F', 'model=gpt-image-1',
     '-F', `image[]=@${swatchPath}`,
-    '-F', 'n=3', '-F', 'size=1024x1536', '-F', 'quality=medium',
+    '-F', 'n=1', '-F', 'size=1024x1536', '-F', 'quality=medium',
     '-F', `prompt=${PROMPT}`, '--max-time', '300',
   ], { maxBuffer: 64 * 1024 * 1024 }).toString();
   return JSON.parse(out);
@@ -64,8 +64,8 @@ function genThree(swatchPath) {
 
 async function processItem(it, idx, total, tally) {
   const dir = join(OUT, it.handle);
-  if (existsSync(join(dir, '1.png')) && existsSync(join(dir, '2.png')) && existsSync(join(dir, '3.png'))) {
-    log(`SKIP ${idx}/${total} ${it.handle} (already has 3)`); return;
+  if (existsSync(join(dir, '1.png'))) {
+    log(`SKIP ${idx}/${total} ${it.handle} (already has image)`); return;
   }
   mkdirSync(dir, { recursive: true });
   const swatch = join(TMP, it.handle + '.png');
@@ -75,8 +75,8 @@ async function processItem(it, idx, total, tally) {
       const j = genThree(swatch);
       if (j.error) throw new Error(JSON.stringify(j.error).slice(0, 160));
       const data = j.data || [];
-      if (data.length < 3) throw new Error('got ' + data.length + ' images');
-      data.slice(0, 3).forEach((d, i) => writeFileSync(join(dir, `${i + 1}.png`), Buffer.from(d.b64_json, 'base64')));
+      if (data.length < 1) throw new Error('got ' + data.length + ' images');
+      data.slice(0, 1).forEach((d, i) => writeFileSync(join(dir, `${i + 1}.png`), Buffer.from(d.b64_json, 'base64')));
       const c = price(j.usage); tally.cost += c; tally.done++;
       log(`OK   ${idx}/${total} ${it.handle}  +$${c.toFixed(3)}  running=$${tally.cost.toFixed(2)}  done=${tally.done}`);
       return;
@@ -98,7 +98,7 @@ async function pool(items, size, worker) {
 
 (async () => {
   const items = await fetchMajilite();
-  log(`START batch: ${items.length} Majilite items × 3 images (concurrency 3)`);
+  log(`START batch: ${items.length} Majilite items × 1 image (concurrency 3)`);
   const tally = { cost: 0, done: 0, fail: 0, failed: [] };
   await pool(items, 3, (it, idx, total) => processItem(it, idx, total, tally));
   writeFileSync(MAN, JSON.stringify({ finished_at: new Date().toISOString(), items: items.length, generated: tally.done, failed: tally.failed, est_cost_usd: +tally.cost.toFixed(2) }, null, 2));
diff --git a/scripts/gen-alt-tags.mjs b/scripts/gen-alt-tags.mjs
new file mode 100644
index 0000000..fe4bafa
--- /dev/null
+++ b/scripts/gen-alt-tags.mjs
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+// gen-alt-tags.mjs — write alt tags for the Majilite jewelry-case images.
+// Alt phrasing per Steve: "jewelry display cases and fabrics". $0, no API, re-runnable.
+// Writes majilite-jewelry/alt-tags.json (handle→{image,alt}) + an alt.txt in each existing folder.
+import { writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const OUT = join(ROOT, 'majilite-jewelry');
+mkdirSync(OUT, { recursive: true });
+
+// Derive a display title from the handle folder (majilite-stature-rose-gold → "Stature Rose Gold").
+const FIX = { fx: 'FX', ii: 'II', iii: 'III', usa: 'USA', led: 'LED', '3d': '3D' };
+const titleCase = handle => handle.replace(/^majilite-/, '').split('-').filter(Boolean)
+  .map(w => FIX[w.toLowerCase()] || (w.charAt(0).toUpperCase() + w.slice(1))).join(' ');
+
+const altFor = title => `${title}, Majilite metallic wallcovering — jewelry display cases and fabrics`;
+
+// Scan the generated item folders (robust, $0, no network — always matches the real images).
+const handles = readdirSync(OUT, { withFileTypes: true })
+  .filter(e => e.isDirectory()).map(e => e.name).sort();
+
+const map = {};
+let wrote = 0;
+for (const handle of handles) {
+  const title = titleCase(handle);
+  const alt = altFor(title);
+  map[handle] = { title, image: `${handle}/1.png`, alt };
+  const dir = join(OUT, handle);
+  if (existsSync(join(dir, '1.png'))) { writeFileSync(join(dir, 'alt.txt'), alt + '\n'); wrote++; }
+}
+writeFileSync(join(OUT, 'alt-tags.json'), JSON.stringify(map, null, 2));
+console.log(`alt-tags.json: ${handles.length} item folders; alt.txt written to ${wrote} with an image`);
+console.log('sample:', altFor(titleCase(handles[0] || 'majilite-stature-rose-gold')));

← 03e14be Maya Romanoff flip book (materials story, 24 SKUs, 4 chapter  ·  back to Dw Marketing Reels  ·  Storefront: DW Collection Films custom-liquid section (flip 1cda4a2 →