← back to Crazy News Channel
scripts/daily-cartoon-gen.mjs
87 lines
#!/usr/bin/env node
// daily-cartoon-gen.mjs — picks TODAY's 6 P24 Political Cartoon briefs from the
// rotating bank (cartoon-prompt-bank.mjs), skipping anything already built
// (cartoons/manifest.json), and writes the day's queue to cartoons/queue/<date>.json.
//
// $0 — pure local selection, no model calls. This is STEP 1 of the pipeline; it
// only decides WHAT to build today. Building each cartoon (STEP 2) is a separate,
// heavier operation (currently: an interactive Claude session per brief, same
// process as TK-12111's form-to-request-a-form) — see README-cartoons.md.
//
// Usage: node scripts/daily-cartoon-gen.mjs [--count=6] [--date=YYYY-MM-DD]
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { CARTOON_BANK } from './cartoon-prompt-bank.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const MANIFEST = path.join(ROOT, 'cartoons', 'manifest.js');
const QUEUE_DIR = path.join(ROOT, 'cartoons', 'queue');
const args = Object.fromEntries(process.argv.slice(2).map(a => {
const m = a.match(/^--([^=]+)=(.*)$/);
return m ? [m[1], m[2]] : [a.replace(/^--/, ''), true];
}));
const COUNT = parseInt(args.count || '6', 10);
const DATE = args.date || new Date().toISOString().slice(0, 10);
function loadManifest() {
// manifest.js is `window.P24_CARTOONS = [ ...json... ];` — extract just the array
// literal rather than fetch()/require()-ing it (this file has no DOM `window`, and
// the array itself is plain JSON so a regex+JSON.parse is safe and dependency-free).
try {
const src = fs.readFileSync(MANIFEST, 'utf8');
const m = src.match(/window\.P24_CARTOONS\s*=\s*(\[[\s\S]*\])\s*;\s*$/);
return { cartoons: m ? JSON.parse(m[1]) : [] };
} catch { return { cartoons: [] }; }
}
function seededShuffle(arr, seed) {
// deterministic per-day shuffle so re-running the same date is idempotent
let s = seed;
const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function dateSeed(d) {
return d.split('-').reduce((acc, n) => acc * 31 + parseInt(n, 10), 7);
}
function main() {
const manifest = loadManifest();
const built = new Set(manifest.cartoons.map(c => c.id));
const available = CARTOON_BANK.filter(([id]) => !built.has(id));
const pool = available.length >= COUNT ? available : CARTOON_BANK; // wrap around once the bank is exhausted
const picked = seededShuffle(pool, dateSeed(DATE)).slice(0, COUNT);
fs.mkdirSync(QUEUE_DIR, { recursive: true });
const queueFile = path.join(QUEUE_DIR, `${DATE}.json`);
const queue = {
date: DATE,
generated_at: new Date().toISOString(),
count: picked.length,
briefs: picked.map(([id, title, brief, category]) => ({
id, title, brief,
// category is prep-only carried through as of TK-12142 (2026-09-24) — the
// picker above is still a flat shuffle across the whole bank, it does not
// group/weight by category. Defaults to 'politics' for any pre-existing
// 3-tuple bank entry so this stays backward compatible.
category: category || 'politics',
slug: `${DATE.replace(/-/g, '')}-${id}`,
status: 'queued',
})),
};
fs.writeFileSync(queueFile, JSON.stringify(queue, null, 2));
console.log(`Queued ${picked.length} cartoon(s) for ${DATE} -> ${path.relative(ROOT, queueFile)}`);
for (const b of queue.briefs) console.log(` - [${b.id}] ${b.title}`);
return queue;
}
main();