overlay-factory-worker
v0.1.0
Published
The Goose Tools Overlay Factory worker — your computer renders reel overlays for goosetools.com with Remotion and ffmpeg.
Readme
reel-factory
Automated overlay editing for Erin's reel series. Feed it b-roll + a code card + an episode JSON, get a finished vertical MP4 with the "What Prints?" title, the code card, and a timed difficulty reveal.
Flow
npx tsx scripts/make-card.ts puzzles/x.json # write a NEW code card from a puzzle JSON
./scripts/ingest.sh # pull assets from ~/Desktop/Automate Editing (via Finder)
npx tsx scripts/prep-card.ts # code-card JPGs -> transparent rounded PNGs
npx tsx scripts/new-episode.ts # picks next card + b-roll, places the meter
npx remotion render WhatPrints out/what-prints-XXX.mp4 --props=episodes/what-prints-XXX.json
./scripts/export.sh out/what-prints-XXX.mp4 # copy to Desktop/Automate Editing/exportsPreview while tweaking: npm run dev (Remotion Studio).
Episode JSON
{
"broll": "assets/b-roll/IMG_4858.MOV",
"card": "assets/cards/IMG_4030.png",
"titleLine1": "What",
"titleLine2": "Prints?",
"difficulty": "hard", // easy | medium | hard
"durationSec": 10,
"revealAtSec": 4, // when the difficulty meter pops in
"trimBeforeSec": 0, // skip into the b-roll clip
"meterX": 70, // set by scripts/place-overlay.ts
"meterY": 670
}episodes/index.json maps every code card to its episode metadata (answer, why,
caption) extracted from the caption screenshots. episodes/used.json tracks
which cards/b-roll are consumed.
Generating code cards
The original cards were phone screenshots of a chat app's code block, cropped by
prep-card.ts. New ones are rendered directly: write a puzzle JSON in puzzles/
and run scripts/make-card.ts. It highlights the snippet with Shiki (any
language), renders the CodeCard composition to a transparent rounded PNG at
public/assets/cards/<slug>.png, and upserts the entry into cards.json and
index.json — so re-running the same slug edits in place.
{
"slug": "nan-trap",
"name": "Bowser's NaN Trap",
"language": "Python",
"code": "score = float(\"nan\")\nbest = score\n\nprint(score == best)",
"answer": "False",
"why": "NaN is never equal to itself...",
"difficulty": "easy"
}Every card is executed before it renders and put through scripts/quality.ts:
the snippet must print exactly what answer claims, print the same thing
three runs in a row and on every installed Python 3.11-3.14, be answerable in a
comment (no memory addresses, nothing over 120 chars), not duplicate an existing
card, and carry a real why. Any failure stops the render; --no-verify and
--no-gates override. Audit the whole library with npx tsx scripts/quality.ts
or npx tsx scripts/verify.ts.
Cards that fail get a retired note in index.json rather than being deleted —
new-episode.ts skips them, and the audits ignore them.
Keep lines under ~45 characters and snippets under ~8 lines — beyond that the
script shrinks the type to fit and warns. Colors and geometry live in
src/series/what-prints/codeCardTypes.ts; preview with npm run dev.
The /what-prints-card skill writes the puzzles and drives this end to end.
Engagement data
npx tsx scripts/ig-sync.ts pulls the Instagram history for @erin.codes into
episodes/performance.json — captions, likes, comments, and per-post insights
(reach, saves, shares, views). Read-only; the token comes from
~/code/crossposter/.env (the ig-auto-dm one has expired).
Insights calls are rate-limited, so by default it only fetches them for posts
whose caption looks like a series episode. Use --since=2026-06-01 for a date
range or --all for every reel; results already fetched are reused.
npx tsx scripts/report.ts then ranks the published episodes against the
account's own baseline and links each post back to the card it came from
(captions are the join key; set permalink on an index.json entry to force it).
Judge cards by comments per 1,000 reach — reach swings by orders of magnitude and likes track reach, so raw likes say more about distribution than about whether the puzzle landed.
Overlay Factory
The overlay components (src/overlay/) are shared: What Prints composites them
over its own b-roll, and the Overlay Factory tool on Goose Tools burns them onto
any clip uploaded from a phone.
Burn overlays onto a clip locally:
npm run overlay -- clip.mov overlay.json out.mp4Run the worker so /dashboard/overlay can send jobs to this machine. The token
comes from ~/.goosetools/env, where the Carousel Maker's installer already
put it — one token serves every Goose Tools worker on a machine:
npm run overlay:worker # foreground, stops when the terminal closes
npm run overlay:install # keep it running across reboots (launchd)
npm run overlay:uninstall # stop it and remove the agentoverlay:install writes ~/Library/LaunchAgents/com.goosetools.overlay.plist,
matching the Carousel/Caption/Brand agents. Logs land in
~/.goosetools/overlay-worker.log. The plist sets PATH explicitly — launchd
starts with an almost-empty environment, and the render shells out to ffmpeg,
npx and claude, so an inherited-PATH assumption is what leaves one of these
agents running but failing every job.
Two passes, and the split is the point. Remotion renders only the overlay,
alpha-only to ProRes 4444, at --scale so the 1080x1920 composition rasterizes
natively at the output resolution. ffmpeg then composites that over the
untouched source. The footage never enters Chrome, so a 4K clip stays 4K and
renders in seconds rather than minutes — and every coordinate in the overlay
components stays in 1080x1920 space no matter the output size.
Output is HEVC in yuv420p. That pixel format isn't cosmetic: the overlay
filter hands back BGRA, and an HEVC file in BGRA imports into iOS Photos as a
file that won't play.
Series
What Prints? is special-cased in the worker — it runs your snippet, checks the answer against a real run, applies the quality gates, and places the meter against the footage. Nothing generated can do that, which is why it stays code.
Every other series is generated. Tapping "+ New series" on Goose Tools queues a
job; this machine reads your reference images with Claude and writes
series/<slug>/:
series/hot-take/
series.json the fields the form asks for, and where each layer sits
claim.html self-contained HTML+CSS, {{field}} placeholders
verdict.htmlseries/ is gitignored — each worker builds its own from its own user's
references, so no two machines have the same set.
The hard rule for templates is no CSS animation. Remotion renders by seeking
to each frame and screenshotting, so a CSS animation or transition freezes on
whatever the first frame painted. Motion comes from the layer's enter value
(fade, slide-up, slide-down, pop), which the composition drives with
Remotion's own spring. Templates also can't carry <script> or any external
URL; scripts/series.ts rejects a generation that does, and the composition
strips scripts again at render time rather than trusting it.
Instagram's safe area
src/overlay/safeArea.ts holds where Instagram draws over a reel, as
three rectangles rather than one inset margin — the action rail only runs down
the lower half, so a flat "keep the right 260px clear" would flag the title and
card as unsafe when nothing is near them.
| Zone | Area | |---|---| | top bar | full width, y 0-250 | | action rail | x 820-1080, y 1080-1700 | | caption | x 0-820, y 1500-1920 |
npx tsx scripts/check-safe-area.ts episodes/what-prints-00X.json measures the
real overlay bounding boxes out of a render (same two-render diff as the
legibility check) and reports anything intersecting a zone. It also flags
elements outside the 3:4 centre crop that the profile grid uses for thumbnails.
Numbers are conservative — IG moves its chrome between app versions and notched phones differ from flat ones. If something looks wrong on a real phone, adjust the zones there and every check follows.
Does it read on a phone?
npx tsx scripts/check-legibility.ts episodes/what-prints-00X.json renders three
frames twice — once whole, once with hideOverlays so only the background
draws — diffs them to find the overlay pixels, and measures real contrast.
Text (the title) is scored on local contrast: strokes against what's immediately around them in the finished frame, so the dark halo counts. Panels (the code card, the difficulty pill) are scored on how well the opaque block separates from the footage, because their internal contrast is fixed by design. Thresholds are calibrated against the known-bad title, which measured 4.3:1 and would have passed a textbook WCAG bar; the fixed one measures 12.3:1.
new-episode.ts runs this automatically after placing the meter.
Notes
- macOS blocks direct shell reads of
~/Desktop(TCC). All Desktop I/O goes through Finder AppleScript — that's why ingest/export useosascript. - Exports are silent; add trending audio in Instagram.
- The title and code card are in fixed positions every episode (title centred
200px from the top, card centred at 86% width). The difficulty meter has a
house position too — lower-left at (70, 1400) — and
scripts/place-overlay.tsonly moves it when the footage there is more than 25% busier than the calmest spot available. The comparison is relative because footage with Erin in frame scores high everywhere. Override by editing meterX/meterY. - New series (Interviewer, Games): add
src/series/<name>/with its own composition, register it insrc/Root.tsx, same episode-JSON pattern.
