← back to Wallco Ai
scripts/bulk-bg-swap.js
93 lines
#!/usr/bin/env node
// bulk-bg-swap — apply ONE texture to MANY designs in a category.
//
// Pulls all published wallco.ai designs in the named category, hits
// /api/design/:id/replace-bg with the given texture image URL (or slug),
// concurrency-3 to stay polite with Gemini. Logs progress + results.
//
// Usage:
// node scripts/bulk-bg-swap.js --category drunk-animals --texture-url URL --texture-name "Topanga Canyon" [--limit 50] [--concurrency 3]
// node scripts/bulk-bg-swap.js --category drunk-animals --texture-slug cream-grasscloth
'use strict';
const fs = require('fs');
const path = require('path');
const { psqlQuery } = require('../lib/db.js');
const SERVER = process.env.WALLCO_SERVER || 'http://127.0.0.1:9877';
const ARGS = process.argv.slice(2);
const arg = n => { const i = ARGS.indexOf('--' + n); return i >= 0 ? ARGS[i+1] : null; };
const CATEGORY = arg('category');
const TEXTURE_URL = arg('texture-url');
const TEXTURE_SLUG= arg('texture-slug');
const TEXTURE_NAME= arg('texture-name') || (TEXTURE_SLUG || 'custom');
const LIMIT = parseInt(arg('limit') || '500', 10);
const CONCURRENCY = parseInt(arg('concurrency') || '3', 10);
if (!CATEGORY) { console.error('--category required'); process.exit(2); }
if (!TEXTURE_URL && !TEXTURE_SLUG) { console.error('--texture-url OR --texture-slug required'); process.exit(2); }
function log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); }
async function swap(id) {
const body = TEXTURE_URL
? { texture_image_url: TEXTURE_URL, texture_name: TEXTURE_NAME }
: { texture_slug: TEXTURE_SLUG };
const r = await fetch(`${SERVER}/api/design/${id}/replace-bg`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j.error || `http ${r.status}`);
return j;
}
async function pool(items, n, fn) {
let i = 0;
const next = () => (i < items.length ? items[i++] : null);
async function worker() {
while (true) {
const it = next(); if (!it) return;
try { await fn(it); }
catch (e) { log(` err id=${it} ${e.message.slice(0,140)}`); }
}
}
await Promise.all(Array.from({ length: n }, worker));
}
(async () => {
// Fetch ids
const sql = `SELECT id FROM spoon_all_designs
WHERE is_published=TRUE AND brand='wallco.ai' AND local_path IS NOT NULL
AND category='${CATEGORY.replace(/'/g, "''")}'
ORDER BY id DESC
LIMIT ${LIMIT};`;
const raw = psqlQuery(sql);
const ids = raw.split('\n').filter(Boolean).map(s => parseInt(s, 10)).filter(Number.isFinite);
if (!ids.length) { log(`no designs in ${CATEGORY}`); process.exit(0); }
log(`bulk-bg-swap · category=${CATEGORY} texture=${TEXTURE_NAME} · ${ids.length} designs · conc=${CONCURRENCY}`);
let done = 0, ok = 0, err = 0;
const t0 = Date.now();
const logF = path.join(__dirname, '..', 'logs', `bulk-bg-swap-${CATEGORY}-${Date.now()}.jsonl`);
await pool(ids, CONCURRENCY, async (id) => {
try {
const j = await swap(id);
ok++;
fs.appendFileSync(logF, JSON.stringify({ ts: new Date().toISOString(), src_id: id, new_id: j.new_id, elapsed_s: j.elapsed_s, texture: TEXTURE_NAME, ok: true }) + '\n');
log(` ok #${id} → #${j.new_id} (${j.elapsed_s}s)`);
} catch (e) {
err++;
fs.appendFileSync(logF, JSON.stringify({ ts: new Date().toISOString(), src_id: id, error: e.message.slice(0,200), ok: false }) + '\n');
log(` ERR #${id} ${e.message.slice(0,140)}`);
}
done++;
if (done % 5 === 0 || done === ids.length) {
const rate = done / ((Date.now() - t0) / 1000);
log(`progress ${done}/${ids.length} · ok=${ok} err=${err} · ${rate.toFixed(2)}/s`);
}
});
const wall = ((Date.now() - t0) / 1000).toFixed(1);
log(`done · ${ok} ok · ${err} err · ${wall}s wall · log=${logF}`);
})().catch(e => { console.error('FATAL', e); process.exit(1); });