← back to Wallco Ai

scripts/da4-preview-batch.js

127 lines

#!/usr/bin/env node
/**
 * DA-4: heterogeneous preview batch for the drunk-animals refresh.
 * Generates 6 designs spanning 4 categories, curator-mode (is_published=
 * false because generate_designs.js inserts FALSE by default), local
 * ComfyUI backend ($0). Writes data/logs/drunk-animals-preview-batch.json.
 *
 * Settlement-gate runs inline via scripts/settlement_tick_guard.js, so
 * BLOCK / NEEDS_REVIEW will skip that prompt and try another.
 */
'use strict';
const path = require('path');
const fs = require('fs');
const { spawnSync, execSync } = require('child_process');

const ROOT = path.join(__dirname, '..');
const OUT_JSON = path.join(ROOT, 'data', 'logs', 'drunk-animals-preview-batch.json');

const { buildN: buildDA } = require('./drunk_animal_prompts');
const { buildN: buildV2 } = require('./drunk_monkeys_v2_prompts');
const { buildN: buildSub } = require('./drunk_monkeys_birds_subdued_prompts');
const SHARED = require('./drunk_motif_shared');

// Build a drunk-zoo-36 prompt by lifting ZOO_VIGNETTES from night-builder
// (it's not exported there) and SHARED + composing it here.
const NB = fs.readFileSync(path.join(ROOT, 'scripts', 'night-builder.js'), 'utf8');
const VIGS_MATCH = NB.match(/const ZOO_VIGNETTES = \[([\s\S]*?)\];/);
const VIGS = VIGS_MATCH[1].split(/'\s*,\s*\n\s*'/).map(s => s.replace(/^[\s']*|['\s,]*$/g, ''));
const ARTDIR_MATCH = NB.match(/const SHARED_ARTDIR = ([\s\S]*?);\s*\n/);
const SHARED_ARTDIR = ARTDIR_MATCH ? eval(ARTDIR_MATCH[1]) : '';

const ZOO_CWS = [
  { name: 'Bordeaux Velvet',  tone: 'deep burgundy',     hex: '#5a1818' },
  { name: 'Forest Loden',     tone: 'dark gallery green', hex: '#22443a' },
];

function composeDrunkZoo36(cw) {
  const vig = VIGS[Math.floor(Math.random() * VIGS.length)];
  const tex = SHARED.TEXTURE_GROUNDS[Math.floor(Math.random() * SHARED.TEXTURE_GROUNDS.length)];
  const bb  = SHARED.BOTTLE_BACKDROPS[Math.floor(Math.random() * SHARED.BOTTLE_BACKDROPS.length)];
  const ref = SHARED.pickReferenceClause('drunk-zoo-36');
  return (
    `Drunk Zoo 36" repeat wallpaper — ${cw.name} colorway. ` +
    `Substrate: real ${tex.slug} (${tex.voice.replace(/^rendered on /, '')}). ` +
    `Ground: solid ${cw.tone} (${cw.hex}) tinted over the natural-fiber substrate so the texture grain still reads through. ` +
    `Figure: bone-ivory (#F2EADB) single-ink. ` +
    `Vignette: ${vig}. Renders at wall-scale 36-inch seamless tile — motifs are LARGE, anchored on the central vertical axis of symmetry, generous breathing room of clean ground around each motif. ` +
    (ref ? `Reference anchor: ${ref}. ` : '') +
    `Secondary detail in negative space: ${bb} (rendered as a tiny bone-ivory ink silhouette at the very back, smaller scale than the main pair, not competing). ` +
    `MANDATORY motifs visible in EVERY vignette: (1) alcohol being consumed, (2) a lit handrolled cannabis joint with a visible curl of smoke, (3) the animal(s) swinging from something. ` +
    `Each animal is one FLAT STENCIL SILHOUETTE in bone-ivory ink — no interior shading. The joint and smoke wisp are tiny silhouette props inside the bone-ivory ink layer. NO cannabis leaf, NO marijuana leaf, NO foliage. ` +
    `Whimsical playful joyful tone. Lean toward BOTANICALS over leaves. ` +
    SHARED_ARTDIR
  );
}

// Compose the 6 prompts
const batch = [
  { cat: 'drunk-animals',          prompt: buildDA(1)[0] },
  { cat: 'drunk-animals',          prompt: buildDA(1)[0] },
  { cat: 'drunk-zoo-36',           prompt: composeDrunkZoo36(ZOO_CWS[0]) },
  { cat: 'drunk-zoo-36',           prompt: composeDrunkZoo36(ZOO_CWS[1]) },
  { cat: 'drunk-monkeys-v2',       prompt: buildV2(1)[0].prompt },
  { cat: 'drunk-animals-designer', prompt: buildSub(1)[0].prompt },
];

function psqlMaxId() {
  try {
    return parseInt(execSync(`psql dw_unified -At -c "SELECT COALESCE(max(id),0) FROM spoon_all_designs;"`, { encoding: 'utf8' }).trim(), 10) || 0;
  } catch { return 0; }
}

const results = [];
const startedAt = new Date().toISOString();

for (let i = 0; i < batch.length; i++) {
  const { cat, prompt } = batch[i];
  console.log(`\n[${i+1}/${batch.length}] cat=${cat} — kicking off generate_designs.js`);
  const maxBefore = psqlMaxId();
  const t0 = Date.now();
  const r = spawnSync('node', [
    path.join(ROOT, 'scripts', 'generate_designs.js'),
    '--n', '1',
    '--kind', 'seamless_tile',
    '--category', cat,
    '--prompts', prompt,
  ], { stdio: 'inherit', cwd: ROOT, timeout: 5 * 60_000 });

  const elapsedSec = ((Date.now() - t0) / 1000).toFixed(1);
  let newId = null;
  try {
    const raw = execSync(
      `psql dw_unified -At -c "SELECT id FROM spoon_all_designs WHERE category='${cat.replace(/'/g, "''")}' AND id > ${maxBefore} ORDER BY id DESC LIMIT 1;"`,
      { encoding: 'utf8' }).trim();
    newId = raw ? parseInt(raw, 10) : null;
  } catch {}

  results.push({
    idx: i + 1,
    category: cat,
    new_design_id: newId,
    elapsed_sec: parseFloat(elapsedSec),
    exit_status: r.status,
    view_url: newId ? `http://127.0.0.1:9878/design/${newId}` : null,
    image_url: newId ? `http://127.0.0.1:9878/designs/img/by-id/${newId}` : null,
    prompt_head: prompt.slice(0, 200),
  });

  console.log(`  → ${newId ? `id=${newId} (${elapsedSec}s)` : 'NO NEW DESIGN'}`);
}

const out = {
  generated_at: startedAt,
  finished_at: new Date().toISOString(),
  total_designs: results.length,
  successful: results.filter(r => r.new_design_id).length,
  results,
};

fs.mkdirSync(path.dirname(OUT_JSON), { recursive: true });
fs.writeFileSync(OUT_JSON, JSON.stringify(out, null, 2));
console.log(`\n✓ wrote ${OUT_JSON}`);
console.log(`  ${out.successful} / ${out.total_designs} generated successfully`);
for (const r of results) {
  console.log(`  ${r.category}\tid=${r.new_design_id}\t${r.view_url || '(none)'}`);
}