Blog

Building AnimeRecap: An AI-assisted content pipeline

How I built a daily content pipeline that drafts anime season recaps with Claude, runs ffmpeg over trailer videos for scroll-linked scene frames, and gates every post behind a human review PR.

AI contentAstroFirebaseClaudepipeline

Written by Hong-Bin Yoon · Founder, zzinDev LLC

Published

AnimeRecap is the first product I shipped under zzinDev. It’s a season-level recap blog for anime — each post covers a full season of a show, with plot summaries, character arcs, spoiler-tagged reveals, an editorial rating, and a handful of scroll-linked scene animations cut from the official trailer. At the time of writing it covers a little over a hundred seasons, with more added daily as the pipeline works through a prioritized queue.

This post walks through how it works end-to-end: the actual scripts, the tech choices, the mistakes, and the things I had to add once the site went live that I’d glossed over on paper.

The short version is: three scheduled GitHub Actions workflows (Discovery, Research, Writer), each producing a pull request that I review and merge, with a static Astro site on Firebase Hosting on the other end. The whole stack is boring on purpose.

The problem I was actually trying to solve

Most recap content on the internet falls into three buckets. YouTube essays are great but take 40 minutes when you wanted 5. Fan wikis are exhaustive plot catalogs written for superfans who’ve already watched, not reminder material for someone who watched two years ago. And forum threads are fast-moving debates that get buried.

There’s a gap between “here’s every scene that happened” and “here’s the shape of the story, what it was about, and whether it was worth your time.” I wanted a place to fill that gap at scale.

The hypothesis was that season-level granularity is the right cut. Episode-by-episode coverage is noisy and biases toward superfans; single-paragraph blurbs are useless for anyone who actually wants to remember what happened. A season recap is long-form enough to carry narrative, short enough to read in ten minutes.

The business hypothesis was: this is a content-first SEO play that’s eligible for ad monetization if done honestly. That constrained a lot of later choices.

The three-agent pipeline

The content work is split into three independent agents, each implemented as a TypeScript script invoked from a daily GitHub Action. Each one produces a pull request against main with its output; none of them deploy directly. I am the merge gate.

Discovery runs first. It pulls currently-airing seasons and recent back-catalog releases from the AniList GraphQL API, cross-references with the Jikan (MyAnimeList) API, filters out adult content (there’s a hard policy never to publish isAdult entries, enforced in four independent places so bypassing requires four separate mistakes), deduplicates by franchise (AniList has a lot of weird same-show-different-slug entries — “Attack on Titan” vs “Attack on Titan Season 2” vs “Attack on Titan Final Season Part 2” — all of which should collapse into one canonical anime hub with sequential seasons), and sorts by editorial priority. Output is a PR that edits a single JSON file in the repo: the current discovery queue.

Research picks from the queue. For each target, it fetches plot summaries, character lists, studio credits, and the official trailer URL from AniList. Then it pulls the trailer video via yt-dlp, runs ffmpeg scene detection on it, and extracts a handful of 1–3 second key segments as numbered JPEG frame sequences. Those frames live in /public/images/<anime-slug>/frames/s1-scene-1/, s1-scene-2/, etc. Output is another PR: raw research JSON plus committed frame directories.

Writer is the part everyone asks about. It reads the research JSON for one target and calls Claude via the local claude CLI (not the Anthropic SDK — I’ll come back to why) with a structured prompt that asks for: a TL;DR paragraph, per-arc summary with episode ranges, character development notes, spoiler-tagged reveals, a list of highlight moments, and a rating from 0 to 10 with a one-line justification. The prompt includes a fixed output schema — a YAML frontmatter block plus markdown body — so the output drops straight into src/content/posts/<slug>/season-N.md without post-processing. Output is a PR with the markdown file ready for review.

Each agent has one job, owns one data artifact, and produces one pull request. That separation makes it trivial to fix a broken stage without rerunning the others.

Why claude CLI, not the SDK

The writer calls the claude command-line tool (the one that Claude Code itself runs on) rather than @anthropic-ai/sdk with an API key. Three reasons, in order of importance.

One, cost. My Claude subscription covers unlimited usage via the CLI; per-token API billing does not, and the writer pipeline generates tens of thousands of tokens of output per day. For a one-person side project, the math is stark.

Two, no secrets in CI. The CLI authenticates with an interactive login on my dev box; no ANTHROPIC_API_KEY ever needs to live in GitHub Secrets or leak into a CI runner. The downside is that the writer runs locally, on my machine, not in GitHub Actions — but I’d rather run a script on my laptop every morning than leak a key.

Three, it’s just a script. claude -p "<prompt>" reads from stdin or a prompt file, writes to stdout. That’s easier to reason about than learning another SDK’s abstractions over a surface area that keeps evolving.

The CLI setup does mean the writer is the one stage that doesn’t live in GitHub Actions. Discovery and Research run on the schedule in CI; Writer I invoke by hand from a terminal, on the target slug the Research PR is ready for. In practice that’s a morning ritual, not a friction point.

The human review layer

Every PR the pipeline opens has me as the default reviewer. I never merge on the first read. What I actually catch, in decreasing frequency:

  • Character names spelled inconsistently across paragraphs, usually because AniList’s romanization differs from what the show uses. The model picks one and sticks with it within a draft, but it can pick the wrong one.
  • Spoiler tags placed too early, before the “TL;DR contains spoilers” line that would have made the tag redundant. I re-arrange.
  • Plot summaries that compress a big reveal into a half-sentence you’d miss if you weren’t already watching. I expand.
  • Confident claims about the ending of a show the model hasn’t seen the final episodes of — this one bit me twice before I started cross-referencing air dates against the post date.
  • Purple prose that slipped in. I cut it hard.
  • Ratings that don’t match the show’s actual reception. I override.

None of this is AI slop you can catch by pattern-matching — it’s the kind of thing that requires having some opinion about the show. That’s the editorial judgment, and it’s the product. The AI draft is the starting point.

The site discloses this openly, both on the dedicated Editorial Standards page and in the byline on every post (“Edited by Hong-Bin Yoon, Founder, zzinDev LLC”). Whether or not disclosure changes how a reader feels about it is up to them; I think it should be the norm.

The front-end

The site is Astro 5 with Tailwind and Pagefind for client-side search. Static site generation, no server, no framework runtime. Build output is ~290 static HTML files, deployed to Firebase Hosting via a GitHub Action that runs on every push to main. The whole build takes about 30 seconds including the Pagefind index.

The one custom piece is the scroll-linked scene animations. Each recap post declares a scrollScenes frontmatter array naming a frame directory and frame count. A single Astro component renders an absolute-positioned JPEG stack and a scroll-driven intersection observer steps through the frames based on scroll position. A 3-second scene becomes a 15-frame sequence that plays at reading speed, not autoplay-speed, so it doesn’t steal attention the way an embedded video would.

Pre-rendered frames instead of video was a deliberate choice. Video players are heavy, eat bandwidth, get autoplay-blocked, and their controls clash with a reading layout. Fifteen JPEGs totaling maybe 400KB give you something that loads instantly, works without JavaScript, and reads as ambient motion rather than content I have to control.

What’s not in the pipeline

Things I decided early not to build, and haven’t regretted:

  • No commenting system. Comments on a content site are a moderation problem with no commensurate upside for a one-person team.
  • No paywall. The monetization hypothesis is display ads on well-SEO’d pages; a paywall changes the content strategy entirely.
  • No user accounts. Nothing to sync, nothing to store, nothing to secure.
  • No translations. Every translation multiplies editorial work; English-first until the economics demand otherwise.

Each of these is a deliberate non-goal, not something I’m getting around to. For a one-person project, what you refuse to build is as important as what you ship.

What I’d do differently

A few things in retrospect.

The discovery pipeline’s franchise deduplication logic should have been its own tested utility from day one. I put it inline in the discovery script and spent the first month fixing edge cases where an obviously-same show got counted twice because one entry had “Season 2” in the title and the AniList relation graph didn’t link them. Extracting that logic into pipeline/franchise-dedup.ts with unit tests made the problem go away.

The writer’s output schema should be validated before the PR opens, not during the review. I had the model occasionally produce YAML frontmatter that was ALMOST valid — a missing comma in a genre array, a date in the wrong format. Catching those on the write side instead of the review side is a 10-line Zod schema I should have added at the start.

And the thing I’ve been meaning to do but haven’t: an automated pre-publish check that verifies every plot summary paragraph references at least one character who is in the character list for that season. When the model hallucinates, it usually invents a minor character. That heuristic would catch most of it.

Those are all small refactors. The core architecture — three agents, three PRs, human as the merge gate — has held up.

If you want to see it

AnimeRecap is live at animerecap.zzin.dev. The Editorial Standards page is the short version of this post. If you spot a factual error in any recap, every post has a “Suggest an edit” link at the bottom that opens a pre-filled email — that’s the canonical feedback path.

Spot an error or have a suggestion? Request an edit →

← More writing