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