← back to Trending Dw
scripts/plan-real-images.js
57 lines
#!/usr/bin/env node
// plan-real-images.js — build the work manifest for the real-image fan-out.
// Reads bestsellers.json + ledger, emits data/real-images/manifest.json: every item that
// still lacks a qc-passed real image, grouped into agent-sized batches by host class,
// with an up-front Exa cost estimate (metered — Steve's always-show-costs rule).
// Default is a dry-run report; --write persists the manifest.
const fs = require('fs');
const path = require('path');
const { RI_DIR, MANIFEST, fingerprint, classifyUrl, loadBest, loadLedger, atomicWrite } = require('./real-lib');
const WRITE = process.argv.includes('--write');
const BATCH_SIZE = 12; // items per fan-out agent — keeps each agent's tool budget sane
const EXA_COST_PER_CALL = 0.005; // assumed $/request (search or fetch)
const EST_CALLS_PER_ITEM = { 'etsy-product': 1, 'spoonflower-product': 1, etsy: 2, spoonflower: 2, patternbank: 2, listicle: 2, vendor: 2, none: 2 };
const data = loadBest();
const ledger = loadLedger();
const work = [];
for (const it of data.items){
const fp = fingerprint(it);
const l = ledger[fp];
if (l && l.status === 'qc-pass') continue; // already done — idempotent re-runs
const { host, klass } = classifyUrl(it.url);
work.push({ id: it.id, fingerprint: fp, title: it.title, company: it.company,
marketplace: it.marketplace, style: it.style, color: it.color,
url: it.url || null, host, urlClass: klass });
}
// group by class, then chunk into batches
const byClass = {};
for (const w of work){ (byClass[w.urlClass] = byClass[w.urlClass] || []).push(w); }
const batches = [];
for (const [klass, items] of Object.entries(byClass)){
for (let i = 0; i < items.length; i += BATCH_SIZE){
batches.push({ batch: `${klass}-${Math.floor(i / BATCH_SIZE) + 1}`, urlClass: klass, items: items.slice(i, i + BATCH_SIZE) });
}
}
let estCalls = 0;
for (const w of work) estCalls += EST_CALLS_PER_ITEM[w.urlClass] || 2;
const estCost = estCalls * EXA_COST_PER_CALL;
console.log(`items needing a real image: ${work.length} of ${data.items.length}`);
for (const [klass, items] of Object.entries(byClass)) console.log(` ${klass.padEnd(20)} ${items.length}`);
console.log(`batches: ${batches.length} × ≤${BATCH_SIZE} items`);
console.log(`est. Exa spend: ~${estCalls} calls × $${EXA_COST_PER_CALL} ≈ $${estCost.toFixed(2)}`);
if (WRITE){
fs.mkdirSync(RI_DIR, { recursive: true });
atomicWrite(MANIFEST, JSON.stringify({ plannedAt: new Date().toISOString(), count: work.length,
estExaCalls: estCalls, estExaCostUsd: +estCost.toFixed(2), batches }, null, 2));
console.log(`manifest → ${path.relative(process.cwd(), MANIFEST)}`);
} else {
console.log('(dry-run — pass --write to persist the manifest)');
}