← back to Majilite Jewelry Cases
scripts/gen-batch.mjs
87 lines
#!/usr/bin/env node
// Build ONE jewelry-display-case render per item for: all metallics + all Novasuede.
// Reference-image-to-image via Replicate google/nano-banana (~$0.039/img). Resume-safe.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const OUT = path.join(ROOT, 'output');
const PRICE = 0.039;
const RT = (fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8')
.match(/^REPLICATE_API_TOKEN=(.+)$/m) || [])[1]?.trim().replace(/"/g, '');
if (!RT) { console.error('No REPLICATE_API_TOKEN'); process.exit(1); }
const METAL_RX = /metallic|silver|pearl|foil|chrome|\bgold\b|bronze|copper|platinum|shimmer|apollo|finesse|drizzle|celestial|brushed|mica|glitter|luster|lustre|iridescent|frost|sterling|pewter|titanium|steel|glimmer|attache|beton|burnished|capricorn|chinchilla|cross-hatch|deco|echo|eclipse|farro|stardust|leaf/i;
const FIDELITY =
'The material is the interior lining/upholstery of the display, not a backdrop. ' +
'Photorealistic luxury jewelry catalog photography, glass-topped boutique display case, ' +
'watches, pendants and rings arranged on the lined risers, warm high-end retail lighting, ' +
'no text, no watermark, no logos, no hands.';
const promptFor = (it) => {
const metal = METAL_RX.test(it.title);
const mat = metal
? 'a premium METALLIC synthetic suede microfiber material'
: 'a premium synthetic suede microfiber material';
const crit = metal
? 'CRITICAL: reproduce the EXACT metallic color, shimmer/sheen and fine suede nap shown in the provided material swatch image.'
: 'CRITICAL: reproduce the EXACT color, sheen and fine suede nap shown in the provided material swatch image.';
return `A glass-topped boutique jewelry counter display case whose interior base and risers are lined in ${mat}. ${crit} ${FIDELITY}`;
};
const safe = s => String(s ?? 'item').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function genOne(prompt, swatchUrl) {
for (let a = 1; a <= 4; a++) {
const r = await fetch('https://api.replicate.com/v1/models/google/nano-banana/predictions', {
method: 'POST',
headers: { Authorization: `Bearer ${RT}`, 'Content-Type': 'application/json', Prefer: 'wait' },
body: JSON.stringify({ input: { prompt, image_input: [swatchUrl], output_format: 'png' } }),
});
const j = await r.json();
if (j.status === 'succeeded' && j.output) {
const url = Array.isArray(j.output) ? j.output[0] : j.output;
const img = await fetch(url); return Buffer.from(await img.arrayBuffer());
}
if (r.status === 429 || r.status >= 500 || j.status === 'starting' || j.status === 'processing') { await sleep(2500 * a); continue; }
throw new Error(`replicate ${r.status} ${j.status || ''}: ${JSON.stringify(j.error || j).slice(0, 160)}`);
}
throw new Error('exhausted retries');
}
async function main() {
const all = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/items.json'), 'utf8'));
// union: metallic (any vendor) + all Novasuede; dedupe by sku
const ALL = process.argv.includes('--all');
const seen = new Set(), items = [];
for (const x of all) {
const key = x.sku || x.title;
if (seen.has(key)) continue;
if (ALL || METAL_RX.test(x.title) || x.vendor === 'Novasuede') { seen.add(key); items.push(x); }
}
console.log(`Target items: ${items.length} (metallics + all Novasuede, 1 render each)`);
let made = 0, skip = 0, fail = 0, spend = 0;
for (let i = 0; i < items.length; i++) {
const it = items[i];
const dir = path.join(OUT, `${safe(it.vendor)}__${safe(it.sku || it.title)}`);
fs.mkdirSync(dir, { recursive: true });
const f = path.join(dir, 'glass-counter.png');
if (fs.existsSync(f)) { skip++; continue; }
try {
const png = await genOne(promptFor(it), it.image_url);
fs.writeFileSync(f, png);
made++; spend += PRICE;
console.log(` [${i + 1}/${items.length}] ${it.vendor} ${it.title} ($${spend.toFixed(2)})`);
} catch (e) { fail++; console.log(` ! ${it.sku}: ${e.message}`); }
await sleep(300);
}
console.log(`\nDONE. made=${made} skipped=${skip} failed=${fail} est_spend=$${spend.toFixed(2)}`);
}
main().catch(e => { console.error(e); process.exit(1); });