← back to Wallco Ai
scripts/drunk_monkeys_birds_subdued_parallel.js
102 lines
#!/usr/bin/env node
/**
* Max-It parallel runner — fires N drunk-monkey/bird/botanical-subdued designs
* through generate_designs.js with K concurrent workers (default 8).
*
* Differs from drunk_monkeys_birds_subdued_batch.js (its serial sibling):
* - uses child_process.spawn (async, not spawnSync)
* - per-design timeout 600s (was 240s — first 99-batch saw 82 timeouts at 240s)
* - bounded concurrency via a simple promise pool
* - prints heartbeat every ~30s with ok/fail/in-flight counts
*
* Usage: node scripts/drunk_monkeys_birds_subdued_parallel.js [N] [CONCURRENCY]
* defaults: N=82, CONCURRENCY=8
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { buildN } = require('./drunk_monkeys_birds_subdued_prompts');
const { checkPrompt } = require('./settlement-gate');
const N = parseInt(process.argv[2] || '82', 10);
const PARLEL = parseInt(process.argv[3] || '8', 10);
const ROOT = path.join(__dirname, '..');
const CATEGORY = 'drunk-monkeys-birds-subdued';
const LOG = `/tmp/dmbs/parallel-${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
fs.mkdirSync(path.dirname(LOG), { recursive: true });
const t0 = Date.now();
function L(msg) {
const line = `[${new Date().toISOString()}] ${msg}`;
console.log(line);
fs.appendFileSync(LOG, line + '\n');
}
L(`=== drunk-monkeys-birds-subdued PARALLEL: ${N} designs × ${PARLEL} workers ===`);
L(`log file: ${LOG}`);
// Build + filter prompts upfront
const items = buildN(N).filter((it, i) => {
const g = checkPrompt(it.prompt);
if (g.verdict !== 'OK') {
L(`[skip ${i+1}] settlement ${g.verdict} — ${(g.reason||'').slice(0,80)}`);
return false;
}
return true;
});
L(`prompts: ${items.length} (${N - items.length} skipped by settlement-gate)`);
let ok = 0, fail = 0, idx = 0;
const inflight = new Map();
function runOne(item, i) {
return new Promise((resolve) => {
const child = spawn('node', [
path.join(ROOT, 'scripts', 'generate_designs.js'),
'--n', '1',
'--kind', 'seamless_tile',
'--category', CATEGORY,
'--prompts', item.prompt,
], { cwd: ROOT });
let stderr = '';
child.stderr.on('data', d => { stderr += d.toString(); });
const killTimer = setTimeout(() => { try { child.kill('SIGKILL'); } catch (e){} }, 600_000);
child.on('close', (code) => {
clearTimeout(killTimer);
if (code === 0) {
ok++;
L(` ✓ [${i+1}] ok palette=${item.palette} animal=${item.animal_summary.slice(0,50)} (${ok}+${fail}=${ok+fail}/${items.length} ${((Date.now()-t0)/60000).toFixed(1)}min)`);
} else {
fail++;
L(` ✗ [${i+1}] FAIL code=${code} stderr=${stderr.slice(0,160).replace(/\n/g,' ')}`);
}
resolve();
});
});
}
const HEART_MS = 30_000;
const heart = setInterval(() => {
L(` ❤ heartbeat — done=${ok+fail}/${items.length} ok=${ok} fail=${fail} inflight=${inflight.size} elapsed=${((Date.now()-t0)/60000).toFixed(1)}min`);
}, HEART_MS);
(async () => {
// Bounded-concurrency promise pool
const workers = Array.from({ length: PARLEL }, async () => {
while (idx < items.length) {
const myIdx = idx++;
const item = items[myIdx];
inflight.set(myIdx, item);
await runOne(item, myIdx);
inflight.delete(myIdx);
}
});
await Promise.all(workers);
clearInterval(heart);
L(`=== batch done: ${ok} ok / ${fail} fail of ${items.length} (${((Date.now()-t0)/60000).toFixed(1)} min) ===`);
// Refresh designs.json + hot-reload
L('refreshing designs.json snapshot…');
spawn('python3', [path.join(ROOT, 'scripts', 'refresh_designs_snapshot.py')],
{ cwd: ROOT, stdio: 'inherit' });
process.exit(fail === 0 ? 0 : (ok > 0 ? 0 : 1));
})();