← back to Wallco Ai
scripts/bulk-fix-category.js
99 lines
#!/usr/bin/env node
// bulk-fix-category — run the 5-layer-enforced fix-design pipeline on every
// published design in a category. Pure PIL, NO Gemini, ~200ms per design.
//
// 5-layer enforcement honored:
// 1. Calls fix-design.js (canonical script)
// 2. Which uses opacity-snap algorithm (preserve composition)
// 3. Never calls /smart-fix?legacy=1 or /regenerate-reverse
// 4. Tags each result with generator='pil-fix-design' (caught by status-browser redo-flag regex as SAFE)
// 5. Logs every fix to data/fix-decisions.jsonl via the appendFixEvent path
//
// Usage:
// node scripts/bulk-fix-category.js --category drunk-animals
// node scripts/bulk-fix-category.js --category drunk-animals --limit 10 --concurrency 4 --k 4
// node scripts/bulk-fix-category.js --category drunk-animals --dry-run
'use strict';
const fs = require('fs');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
const { psqlQuery } = require('../lib/db.js');
const ARGS = process.argv.slice(2);
const arg = (n, d) => { const i = ARGS.indexOf('--' + n); return i >= 0 ? ARGS[i+1] : d; };
const CATEGORY = arg('category');
const LIMIT = parseInt(arg('limit', '500'), 10);
const CONCURRENCY = Math.min(8, Math.max(1, parseInt(arg('concurrency', '4'), 10)));
const K = parseInt(arg('k', '4'), 10);
const CROP = parseFloat(arg('crop', '1.0'));
const DRY = ARGS.includes('--dry-run');
if (!CATEGORY) { console.error('--category required'); process.exit(2); }
function log(m) { console.log(`[${new Date().toISOString()}] ${m}`); }
// Pull IDs from PG
const sql = `SELECT id FROM spoon_all_designs
WHERE category='${CATEGORY.replace(/'/g, "''")}'
AND is_published=TRUE
AND user_removed IS NOT TRUE
AND brand='wallco.ai'
AND local_path IS NOT NULL
ORDER BY id DESC
LIMIT ${LIMIT};`;
const ids = psqlQuery(sql).split('\n').filter(Boolean).map(s => parseInt(s, 10)).filter(Number.isFinite);
log(`bulk-fix-category · ${CATEGORY} · k=${K} crop=${CROP} conc=${CONCURRENCY} · ${ids.length} designs`);
if (DRY) {
log('DRY-RUN — would run fix-design.js on:');
for (const id of ids.slice(0, 10)) log(` #${id}`);
if (ids.length > 10) log(` ... and ${ids.length - 10} more`);
process.exit(0);
}
// Concurrency pool — spawn N parallel fix-design.js processes
const FIX_SCRIPT = path.join(__dirname, 'fix-design.js');
let cursor = 0, ok = 0, errs = 0;
const t0 = Date.now();
function nextOne() {
if (cursor >= ids.length) return null;
return ids[cursor++];
}
async function worker(workerIdx) {
while (true) {
const id = nextOne();
if (id === null) return;
await new Promise((resolve) => {
const child = spawn('node', [FIX_SCRIPT, '--id', String(id), '--k', String(K), '--crop', String(CROP)], {
stdio: ['ignore', 'pipe', 'pipe'],
});
let out = ''; let err = '';
child.stdout.on('data', d => out += d);
child.stderr.on('data', d => err += d);
child.on('close', (code) => {
const done = ok + errs + 1;
if (code === 0) {
ok++;
const match = out.match(/inserted #(\d+)/);
log(` [w${workerIdx}] ok #${id}${match ? ` → #${match[1]}` : ''} (${done}/${ids.length})`);
} else {
errs++;
const firstErr = (err || out).split('\n').filter(Boolean)[0] || `exit ${code}`;
log(` [w${workerIdx}] ERR #${id}: ${firstErr.slice(0, 140)} (${done}/${ids.length})`);
}
resolve();
});
});
}
}
(async () => {
await Promise.all(Array.from({ length: CONCURRENCY }, (_, i) => worker(i + 1)));
const wall = ((Date.now() - t0) / 1000).toFixed(1);
log(`done · ${ok} ok · ${errs} errors · ${wall}s wall · ${(ok / parseFloat(wall)).toFixed(1)}/s`);
})();