← 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));
})();