← back to Wallco Ai

scripts/drunk_monkeys_v2_batch.js

95 lines

#!/usr/bin/env node
/**
 * v2 batch — uses drunk_monkeys_v2_prompts (subject-first, short, explicit
 * cocktail props in prompt body) + the strengthened solid-colors prompt
 * suffix wired into generate_designs.js (2026-05-21 directive).
 *
 * Differs from v1 (drunk_monkeys_birds_subdued_parallel.js):
 *   - Imports v2 prompt builder
 *   - Category = 'drunk-monkeys-v2' (separable from the 'drunk-monkeys-birds-subdued' v1 batch)
 *   - Same 6-worker parallel pool, 600s per-design timeout
 *
 * Usage:  node scripts/drunk_monkeys_v2_batch.js [N] [CONCURRENCY]
 *           defaults: N=100, CONCURRENCY=6
 */
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { buildN } = require('./drunk_monkeys_v2_prompts');
const { checkPrompt } = require('./settlement-gate');

const N      = parseInt(process.argv[2] || '100', 10);
const PARLEL = parseInt(process.argv[3] || '6', 10);
const ROOT     = path.join(__dirname, '..');
const CATEGORY = 'drunk-monkeys-v2';
const LOG = `/tmp/dmbs/v2-${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-v2: ${N} designs × ${PARLEL} workers ===`);
L(`log file: ${LOG}`);

const items = buildN(N).filter((it, i) => {
  const g = checkPrompt(it.prompt);
  if (g.verdict !== 'OK') { L(`[skip ${i+1}] settlement ${g.verdict}`); return false; }
  return true;
});
L(`prompts: ${items.length} (${N - items.length} settlement-skipped)`);

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}] palette=${item.palette}  animal=${item.animal_summary}  (${ok}+${fail}=${ok+fail}/${items.length}  ${((Date.now()-t0)/60000).toFixed(1)}min)`);
      } else {
        fail++;
        L(`  ✗ [${i+1}] FAIL code=${code}  ${stderr.slice(0,140).replace(/\n/g,' ')}`);
      }
      resolve();
    });
  });
}

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`);
}, 30_000);

(async () => {
  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) ===`);
  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));
})();