← back to Crazy News Channel Shadowman
daily-cartoons/README.md
73 lines
# P24 daily editorial cartoons → local review queue (TK-12153, article-driven TK-12237)
4 classic-editorial-ink cartoons/day (cream #cdf5e0 / mint #0f9d78 duotone), each a **still
poster image** satirizing a real story from today's `real-news-data.js` (falling back to
`stories-data.js`'s invented P24 stories when there aren't enough unused real articles),
written to a LOCAL queue for Steve to pick from.
**Nothing here publishes** to p24.agentabrams.com — approving only edits the local site tree.
| Step | Engine | Cost |
|---|---|---|
| articles (pick 1 unused story per cartoon) | `real-news-data.js` + `stories-data.js` via `extract_stories.mjs` | $0 |
| concepts (title, caption, image prompt — one gag per article) | local Ollama (`P24_CARTOON_OLLAMA_MODEL`, default Qwen2.5-Coder-32B) | $0 |
| poster | Replicate `black-forest-labs/flux-schnell`, 4:3 jpg → PIL duotone | ~$0.003 |
~**$0.003/cartoon, ~$0.012/day** at N=4. Still image only — **no video/clip is generated by
default.** A `--video` flag exists (adds a Replicate `wan-2.2-i2v-fast` 5s clip, ~$0.10/cartoon)
but is **opt-in only** and is never passed by `run-daily.sh` or the launchd plist; do not add it
there without an explicit Steve ask (Steve, verbatim, 2026-09-25: "just an image, not moving").
## Article selection
Each cartoon satirizes ONE real (or, as fallback, invented-P24) article, picked by
`generate.py`'s `pick_articles()`:
1. Skip any `story_id` already cartooned in the **last 14 days of the local queue**, or
**anywhere in `cartoons/manifest.js`** (full history, no day window).
2. Prefer real news (`real-news-data.js`), newest first by parsed `publishedLabel`.
3. Fall back to `stories-data.js`'s invented P24 stories only when real-news candidates run out.
4. If there are 0 remaining candidates, the run reports it plainly and exits 1 — it never
silently reuses a story_id.
Each concept is generated from that one article's headline + summary + tags — the prompt
explicitly forbids naming or depicting any real person/party/company/brand/place/org from the
article; invented anonymous figures only (`DENY` regex is a backstop, not the primary guard).
## Run
daily-cartoons/run-daily.sh [N] # default N=4, still image only
python3 daily-cartoons/generate.py 4 --concepts-only # articles + concepts only, $0, no Replicate calls
python3 daily-cartoons/generate.py 4 --video # opt-in: also render a 5s clip per cartoon (~$0.10 ea)
open daily-cartoons/review.html # review page (sort + size slider, created time, cost, linked article)
Outputs: `queue/<date>/<slug>/{poster.jpg, clip.mp4 (only with --video), meta.json}`.
`meta.json` includes `story_id`, `story_title`, `story_url` (the real article link, or the
site's own `index.html#/article/<id>` route for a P24-fallback story), `story_source`
(`real-news` | `p24`), title, caption, prompts, cost, created_at, prediction ids.
## Cost guard
`P24_CARTOON_CAP_USD` (default `1.00`) is a hard **daily** cap summed from `~/.claude/cost-ledger.jsonl`
(`app=p24-daily-cartoons`, today's local date). Each cartoon starts only if its full estimate fits;
each paid call is re-checked. Every paid call is logged via the cost-tracker skill (`log.js`).
`--concepts-only` makes zero paid calls regardless of the cap.
Exit codes: `0` ok · `1` error/no article candidates · `2` cap refused (e.g. `P24_CARTOON_CAP_USD=0 run-daily.sh 1` → exit 2, no spend).
## Approve (local only)
python3 daily-cartoons/approve.py <slug> --date YYYY-MM-DD [--story-id <id>] [--section politics]
Copies media into `cartoons/media/`, writes `cartoons/<date>-<slug>.html`, appends to `cartoons/manifest.js`.
`--story-id` **defaults to the article the cartoon was generated from** (meta.json's `story_id`) and
is still validated against `stories-data.js`/`real-news-data.js`; pass `--story-id` to override. A
story_id is required either way (TK-12158 rule). Works with or without `clip.mp4` — poster-only
cartoons render as a still `<img>`. Deploying to Kamatera stays a separate step Steve runs. Note:
`*.mp4` is gitignored repo-wide.
## Schedule (NOT installed — paste to install; daily 07:23)
cp ~/Projects/crazy-news-channel/daily-cartoons/com.steve.p24-daily-cartoons.plist ~/Library/LaunchAgents/ && launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.steve.p24-daily-cartoons.plist
Remove: `launchctl bootout gui/$(id -u)/com.steve.p24-daily-cartoons`
Distinct from the older text-strip pipeline (`scripts/daily-cartoon-gen.mjs`,
`scripts/com.steve.p24-cartoons-daily.plist`), which only picks briefs from a bank, and from the
older multi-panel interactive-strip cartoons already in `cartoons/manifest.js` (e.g.
`commission-turtles`) built by a different, earlier generator — those have a `#strip`/`#nextBtn`
DOM `scripts/cartoon-shorts/make-cartoon-shorts.mjs` expects; poster-only cartoons from THIS
pipeline don't have that DOM and are not yet compatible with that Shorts script (pre-existing gap,
not caused by --video defaulting off — see TK-12237 notes).