← back to Wallco Ai
scripts/drunk_animals_tick.js
166 lines
#!/usr/bin/env node
/**
* Single-tick drunk-animal design generator — meant to be fired every 4 min
* by ~/Library/LaunchAgents/com.steve.wallco-drunk-animals.plist.
*
* Stops itself at STOP_HOUR (default 6 PT) by exiting 0 without generating
* anything — so launchd silently no-ops past the cutoff.
*
* Usage: node scripts/drunk_animals_tick.js
*/
const { spawnSync, execSync } = require('child_process');
const path = require('path');
const { buildPrompt, ANIMALS, STYLES } = require('./drunk_animal_prompts');
const { pregenGate, postgenVision, quarantine, stampVerdict } = require('./settlement_tick_guard');
// Cross-tick dedup — read the last N drunk-animal prompts from PG and
// extract the animal phrase + style phrase from each. Reject any candidate
// prompt that reuses a recent animal/style. Prevents the "orangutan x3 in
// a row" pattern we saw at 04:20/04:24/04:28.
function recentlyUsed(n = 5) {
try {
const raw = execSync(
`psql dw_unified -At -F'|' -c "SELECT prompt FROM spoon_all_designs ` +
`WHERE category='drunk-animals' ORDER BY id DESC LIMIT ${n};"`,
{ encoding: 'utf8' }
);
const animals = new Set();
const styles = new Set();
for (const line of raw.split('\n').filter(Boolean)) {
for (const a of ANIMALS) if (line.includes(a)) { animals.add(a); break; }
for (const s of STYLES) if (line.includes(s)) { styles.add(s); break; }
}
return { animals, styles };
} catch { return { animals: new Set(), styles: new Set() }; }
}
function freshPrompt() {
const used = recentlyUsed(8);
// Try up to 40 times to dodge both recent animals AND recent styles.
// Fall back to plain buildPrompt if everything's been used (unlikely).
for (let i = 0; i < 40; i++) {
const p = buildPrompt();
const a = ANIMALS.find(x => p.includes(x));
const s = STYLES.find(x => p.includes(x));
if (a && used.animals.has(a)) continue;
if (s && used.styles.has(s)) continue;
return p;
}
// Last resort — just avoid the most-recent animal
for (let i = 0; i < 20; i++) {
const p = buildPrompt();
const a = ANIMALS.find(x => p.includes(x));
const lastAnimal = [...used.animals].pop();
if (a && a === lastAnimal) continue;
return p;
}
return buildPrompt();
}
// Overnight gate removed — drunk-animals now fires 24/7 alongside stoned.
// Steve killed the 18:00→05:59 window on 2026-05-13.
const ROOT = path.join(__dirname, '..');
// Pull source-of-truth bad-aesthetic-patterns.jsonl from prod (the Ghost
// Review UI writes there; cron runs locally because ComfyUI is local). Fails
// open — local stale copy survives if SSH hiccups. ~0.4s when prod reachable.
try { execSync(path.join(__dirname, '_pull-bad-aesthetic.sh'), { stdio: 'ignore', timeout: 8000 }); } catch {}
const { isCategoryCooldown, getAvoidanceAddendum, getCategoryFailureCount } = require('../lib/bad-aesthetic');
// ── Bad-aesthetic cooldown ───────────────────────────────────────────────
// If the drunk-animals category has accumulated ≥ COOLDOWN_THRESHOLD bulk-
// rejected designs in data/bad-aesthetic-patterns.jsonl, skip this tick to
// stop burning Gemini calls on an aesthetic that keeps failing review. Steve
// raises/lowers the bar by tuning DRUNK_ANIMALS_COOLDOWN env var.
const COOLDOWN_THRESHOLD = parseInt(process.env.DRUNK_ANIMALS_COOLDOWN || '50', 10);
const failureCount = getCategoryFailureCount('drunk-animals');
if (isCategoryCooldown('drunk-animals', COOLDOWN_THRESHOLD)) {
console.log(`[tick ${new Date().toISOString()}] COOLDOWN — drunk-animals has ${failureCount} prior failures (threshold ${COOLDOWN_THRESHOLD}). Skipping.`);
process.exit(0);
}
let prompt = freshPrompt();
// If there's any prior failure (even below cooldown threshold), append the
// learned avoidance addendum so SDXL gets explicit "don't repeat the defect".
if (failureCount > 0) {
prompt = prompt + getAvoidanceAddendum('drunk-animals');
console.log(`[tick ${new Date().toISOString()}] LEARNED-AVOID applied (${failureCount} prior failures).`);
}
console.log(`[tick ${new Date().toISOString()}] ${prompt.slice(0, 100)}…`);
// ── Settlement pre-generation gate ───────────────────────────────────────
// No pattern is generated until the gate returns OK. BLOCK / NEEDS_REVIEW
// abort the tick before any image is produced.
const gate = pregenGate(prompt);
if (!gate.ok) {
console.log(`[tick] SETTLEMENT ${gate.verdict} — skipping generation. ${gate.reason}`);
process.exit(0);
}
// Snapshot the max id BEFORE generation so a failed/empty generation can
// never make the post-gen check stamp a pre-existing design.
let maxIdBefore = 0;
try {
maxIdBefore = parseInt(execSync(
`psql dw_unified -At -c "SELECT COALESCE(max(id),0) FROM spoon_all_designs;"`,
{ encoding: 'utf8' }).trim(), 10) || 0;
} catch {}
const r = spawnSync('node', [
path.join(ROOT, 'scripts', 'generate_designs.js'),
'--n', '1',
'--kind', 'seamless_tile',
'--category', 'drunk-animals',
'--prompts', prompt,
// 12 min, not 4: the pipeline is render (~2m, +2m on a gate-triggered retry) →
// quantize → watermark → ghost gate (Gemini) → seam gate → frame gate (Gemini)
// → DB insert. At 4 min the SIGTERM regularly landed BETWEEN the watermark and
// the insert, silently orphaning the PNG on disk with no row and no log line
// (seed 638119258, Jun 3 2026, was one of these — diagnosed 2026-06-09).
], { stdio: 'inherit', cwd: ROOT, timeout: 12 * 60_000 });
// ── Settlement post-generation vision check ──────────────────────────────
// The produced image must be visually verified — text anti-prompts are not
// enforceable on the image model.
if (r.status === 0) {
let designId = null;
try {
const raw = execSync(
`psql dw_unified -At -c "SELECT id FROM spoon_all_designs ` +
`WHERE category='drunk-animals' AND id > ${maxIdBefore} ORDER BY id DESC LIMIT 1;"`,
{ encoding: 'utf8' }).trim();
designId = raw ? parseInt(raw, 10) : null;
} catch {}
if (!designId) {
console.log('[tick] no new design produced (generation backend returned nothing) — nothing to verify');
} else {
let local = null;
try {
local = execSync(
`psql dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=${designId};"`,
{ encoding: 'utf8' }).trim();
} catch {}
const vision = postgenVision(local, prompt);
if (vision.prohibited || vision.error) {
const v = vision.error ? 'REVIEW-VISION-ERROR' : 'BLOCK';
quarantine(designId, v, vision.error
? 'post-gen vision unavailable — fail-closed' : vision.raw);
console.log(`[tick] SETTLEMENT QUARANTINE design ${designId} → ${v}`);
} else {
stampVerdict(designId, vision.verdict === 'OK' ? 'OK' : 'NEEDS_REVIEW');
console.log(`[tick] settlement ${vision.verdict} — design ${designId}`);
}
}
}
// Refresh designs.json snapshot from PG, then ping reload endpoint so the
// live server picks up the new row in its in-memory grid.
spawnSync('python3', [path.join(ROOT, 'scripts', 'refresh_designs_snapshot.py')],
{ stdio: 'inherit', cwd: ROOT, timeout: 60_000 });
spawnSync('curl', ['-sf', '-m', '5', '-X', 'POST', 'http://127.0.0.1:9878/admin/reload-designs'],
{ stdio: 'inherit', cwd: ROOT, timeout: 10_000 });
process.exit(r.status || 0);