Skip to content

TigerMill: quality gates for an AI content pipeline

How I defined acceptance for a novel-to-webtoon pipeline: a gate after every stage, approvals that name who made them, spend behind a human trigger, and tests that guard the defects I already paid for once.

Tim Urista 11 min TigerMill
Title card for Human in the Loop Part 3. A lush anime panel of four adventurers facing an antlered boss and a field of crawlers, labeled rejected, beside the simple 3D blocking for the same shot with only four mannequins.
On this page
  1. What TigerMill is, and what I was managing
  2. Acceptance before output
  3. Five stages, a gate after each
  4. Delegating an approval, then taking it back
  5. The 30-minute policy change
  6. Spend stays behind a human trigger
  7. A dollar of evidence, and the call to repair
  8. What the tests guard
  9. Dollars per accepted panel
  10. What I’d carry into the next pipeline

At a glance

  • TigerMill turns a novel into a webtoon through five stages, each ending at a gate. I wrote acceptance down before judging output: actor inventories, identity locks, and a defect rubric where pretty doesn't score.
  • At 10:13 on Saturday every gate needed a human. At 10:43 I changed the policy: automated actors may close gates, every approval names who made it, and stale writes get a 409.
  • I delegated one approval early, reviewed it, and reversed it. The fix was structural: give the next approval geometry to check against.
  • No tool an automated actor can call spends money. Paid generation is a separate script: dry run by default, --confirm to spend, never retried.
  • 35 Go tests and six rig scripts guard defects that already cost something, like mirrored pose maps and ghosts the server silently refused. The metric I'd manage to next, dollars per accepted panel, isn't recorded yet.

The best-looking image in the TigerMill repo is a rejection. A party of four walks into a crystal cave. An antlered golem glows at the back, crawlers crowd the rocks, and wet light pools on the floor. You could put it on a pitch deck.

I rejected it in one line: the shot contained only the party. The model added a boss and a small army because the style reference had them in it.

That frame is the whole project in miniature. Generating an image is cheap. Deciding what counts as right, and catching the wrong ones before they cost anything, is the actual job. This is Part 3 of Human in the Loop, and it’s about that job: where I put the gates, what I let automation approve, what I reversed, and what the tests are there to guard.

Gated stages

5

Review queue pinned beside all of them

Tools that spend

0 of 13

Paid calls need a human and --confirm

Go tests

35

Plus six rig scripts in make check

Measured fal spend

≈ $1

8 paid requests

What TigerMill is, and what I was managing

Before computers took over, I spent years in middle school learning Renaissance painting technique: charcoal, oil copies of Rembrandt, getting lost chasing the perfect line. So a pipeline whose render stages are literally layout, then line art, then color did not need much selling.

TigerMill turns a novel into a webtoon, the vertical-scroll comic format also called manhwa. One Go process serves the API, a SQLite queue, and exactly one worker that makes one provider call at a time with a three-minute deadline. The browser studio does 3D blocking with Three.js mannequins, storyboards, animatics and a scroll reader. It all fits in a 256 MiB container.

I built it over one weekend. The directory appeared Friday at 08:18, and by Saturday at 19:14 there were 19 commits, an 18-shot and a 36-shot episode in review, and a world bible. Coding assistants drafted much of the code; my part was setting the gates and reviewing what came through them.

TigerMill studio dashboard with a five-stage sidebar, five stage cards labeled You or You or agent, and a counter showing 120 panels ready for review.
The production floor. Each stage card says who may act there, and the footer reads 'Demo rendering · No API spend'. When I took this, 120 panels were waiting on my review.

Acceptance before output

The first four shots that went through image generation taught me what “correct” meant here. Three were rejected outright for inventing enemies. The fourth was held because it invented chasms and platforms. None were rejected for being ugly.

My review note turned that into a contract. Later prompts must carry an explicit inventory of visible actors. Two named crawlers keep fixed world coordinates across shots 4 to 6. A camera cut may move something on screen, but “changed counts/identities/world positions are not” legitimate. That is a spec, and a reviewer can check it without taste entering the argument.

Left: 3D blocking for shot 2 with four simple mannequins walking toward cave columns. Right: a lush anime illustration of the same party facing an antlered golem and dozens of crawlers.
Shot 02. The blocking has four actors. The candidate has four actors, a boss and an army. Rejected against the actor inventory, not on looks.

The provider evaluation pushes further. Candidates get binary defect flags: grip below the guard, blade axis, contact point, occlusion order, limb lengths, digit count, identity locks, placement. The scorer doesn’t know which experiment arm produced the image, and the decision rule is fixed in advance: an arm wins only by reducing structural defects, because “style, palette and prettiness do not count.” The handoff says it shorter: “One attractive image is not evidence.”

This is ordinary engineering management applied to pictures. Write the acceptance criteria before the demo, or the demo writes them for you.

Five stages, a gate after each

One file, navigation.ts, defines the stages: Story & beats, World & cast, Shot & lettering, Structure check, Episode. Each ends at a gate. Review is not a sixth stage. It’s a pinned queue every gate reports into, justified in a line I wish I’d had at a few past jobs: holding review until the end is “the most expensive possible arrangement.”

Stage 4 is labeled “the last free stage.” Its gate checks grip, blade axis, contact point, occlusion and limb length “before one paid pixel.” Everything to its left is local geometry you can redo a hundred times for nothing. A defect visible in a 600px outline costs zero. The same defect found in a color candidate costs a generation plus a review cycle.

Diagram of five stage cards with gates underneath. Gates for script approval and structure check are human-only. Gates for set pinning, composition and episode build can be closed by a human or an automated actor named in the record. Below an amber line, one billed candidate step feeds a review, which either repairs the panel, sends it back to stage 3, or accepts it into the episode.
Who approves what. Outlined gates are human-only; teal gates can be closed by an automated actor, and the record names it. Only the step below the amber line costs money.

Delegating an approval, then taking it back

On Friday I tried delegating the director review of the first three-panel art test. The review came back thorough: all three panels approved, with sensible notes on costume drift and a missing setup beat.

Then I looked at the boss panel myself. The shield, the defender and the attack didn’t physically work together, and good notes don’t fix a pose that can’t happen.

I didn’t quietly edit the verdict. The review file now opens with “Superseded: changes required,” the earlier approvals stay as history, and the reason is written down. Then I changed the process: the art test was rebuilt as an editable 18-shot 3D storyboard, so the next approval would have geometry to check against. I also rejected the lettering as short on dialogue, which became a standing rule: dialogue is planned before art.

The lesson I took is not “never delegate.” It’s that a delegated approval with nothing concrete to check against is a signature, not a review.

The 30-minute policy change

By Saturday morning the rule was strict. At 10:13 the pipeline doc said flatly: “Every gate is a human.” Automated actors could compose shots, record feedback and assemble previews. Approval was mine.

It took half an hour to see that the rule guarded the wrong thing. I was reviewing alongside the work anyway, so every gate was waiting on me for approvals I’d already effectively made. The real risk was never an automated actor pressing approve. It was the record lying about who looked. At 10:43, 0cc6383 Let agents close gates, and record who acted changed the policy: “What makes that safe is attribution, not restriction.”

  • Every review record carries by, with named actors like agent:modelling and agent:rendering.
  • An automated final color approval “reads as asserted by that agent, never as a human reviewer attestation.”
  • Claims let an actor hold panels for 10 minutes by default and an hour at most. Each claim stores the revisions it holds and reports edits made underneath it as stale.
  • Every write names the revision it was based on. A mismatch gets a 409, and “a 409 is information, not an error to retry.”

The tests hold those promises. TestClaimBlocksOthersAndReportsDrift has a human edit a panel under a claim and asserts the holder is told. TestRevisionConflictIsNotRetried asserts the MCP bridge makes exactly one call when it gets a 409. TestActorLabelling sanitizes names like agent:../../etc. One honest caveat from that same test: a request with no actor header is recorded as human. That’s acceptable on a single-user box behind my login. I wouldn’t ship it to a team.

Lollipop chart of 19 commits from Friday 08:00 to Saturday 20:00 with an overnight break, lines changed per commit on a log scale. Teal circles are pipeline and tooling work, rose diamonds are story and world bible work, clustered on Saturday afternoon. Annotations mark Friday's withdrawn approval, no paid generation in agent tools at 08:25, the 10:13 to 10:43 gate policy change, repair over regenerate at 11:59, a ghost save bug caught with a Go test at 18:29, and a capture at 19:14 that rendered a camera inside a character.
The weekend by kind of work, with the review and process calls marked where they landed. Pipeline work in the morning, story in the afternoon, and the story decisions fed straight back into the pipeline.

This is the same shape as the cost attribution work I did at Roku and Apple. Nobody owns a cost they can’t see, and nobody can trust an approval they can’t attribute.

Spend stays behind a human trigger

The MCP server exposes 13 tools. Automated actors can read state, update a scene at a revision, request changes, rebuild or preview an episode, review a panel, and manage claims. None of the 13 can generate an image, retry a paid job, capture the browser or import a candidate. The server’s own instructions end with “No paid generation tools are exposed.”

Paid generation lives in a separate script. Without flags it prints “nothing submitted, nothing billed. Add —confirm to spend.” It never retries, because “a timeout is not a cancellation”: a failed request may already have billed. A retry loop on a billed endpoint is how you pay twice for a panel you’re about to reject.

The store applies the same discipline to review. Each stage gets three attempts. TestReviewLifecycleAndStaleWrites asserts that a rejection without written feedback is refused, a stale approval conflicts, and an approved panel can’t be reopened. After a restart, in-flight jobs become failed and wait for a human decision.

A dollar of evidence, and the call to repair

Late Saturday morning I authorized the conditioning trial against fal: eight paid requests, “roughly a dollar.” Warm FLUX requests came back in 5 to 12 seconds. Cold runs on a custom checkpoint took 128 and 243.

The most humbling result: FLUX.1-dev, given the full character-lock paragraph and no reference, “produced three characters instead of four, no white forelock, no ivory mantle, a bow instead of two short blades, and the bronze shield on the wrong person.”

Left: a polished anime lineup of four characters, Ren, Vela, Io and Pip, with distinct costumes and props. Right: a generic cartoon image of three characters in a cave with swapped props, generated from a prompt only.
Left, the cast the prompt described. Right, what came back from the prompt alone in 11.9 seconds. Adjectives don't carry costume, headcount or props, and the identity locks in the rubric caught every miss.

Img2img at strength 0.8 mostly discarded the reference. IP-Adapter blended four people “into pure noise.” And a CFG value carried from FLUX to SDXL “makes mush,” which is how a parameter mistake gets blamed on the model.

The trial contradicted the plan, so I dropped the plan. Generating from scratch lost to the output we already had, and conditioning turned out to be “a structure tool, not a style tool.” The pipeline switched to repair rather than regenerate: the control pack exports the pose skeleton over the blocking render, “where the limb belongs, against where the candidate put it,” and the ink studio is where the fix gets traced. The handoff’s rule: “When a test contradicts the design, the design is wrong.”

Four panels. A grey 3D mannequin in a two-handed downward stab, then an anime render that holds the pose. A line-art outline of a mannequin standing over a crawler, then an anime render that changes the arm pose and sword direction.
Volumes in, pose kept for review (left pair). Outlines in, pose lost and rejected (right pair). One call each, so it's a lead, not a result. The rear leg on the kept one still drifted.

The 36-shot episode used 3D atlas references, six shots per request, eight requests total. Its review note still says “generated art is a candidate, not a passed consistency check,” and lists what I flagged: crawlers in shot 12 that don’t read as collapsed, a boss in shot 24 half upright. One targeted correction fixed both, and the originals were kept.

Left: a low-poly Three.js atlas of six shots with mannequins on a cave ledge. Right: the finished manhwa panel for shot 9, with Ren stabbing a crystal crawler while Vela holds a shield behind him.
Free blocking in, candidate panel out. Six shots per request cut requests about 4.5x, and every panel still went through review.

What the tests guard

The handoff sets one rule I’d put in any team’s working agreement: “Every bug found gets a test that fails first.” Its reasoning is the part I’d frame: “A test that has never failed has not been shown to catch anything.”

As of Saturday night that meant 35 Go test functions in 1,207 lines, against about 2,500 lines of non-test Go, plus six Node scripts that assert the 3D rig. make check runs the Go suite with the race detector, the rig scripts and vet, and CI runs it on every push and pull request before an image is published. The count matters less than what the tests pin down.

Handedness. The mannequin faces +Z, so its own right side is -X, but the rig names that side left*. An early outline trial was confounded by it. test-pose.mjs now asserts that, facing the camera, the right shoulder, hip and eye project to the viewer’s left, because “a regression here silently mirrors every pose map.”

Ghosts, by subtraction. In the world bible I made brief ghosts background only, a story call and a cost call at once. c5114ef Give brief ghosts a look, built by subtraction reuses the existing mannequin with everything identifying removed. The test asserts fewer meshes, every bone kept, no accent color and no pose figure, so “a corridor full of extras” can’t fight the subject for the control map.

That test passed, and ghosts still couldn’t be saved: the TypeScript type had the flag, the Go struct didn’t, and the API rejects unknown fields. ghost_test.go now decodes exactly the way the API does, and refuses the ghost flag on a cast member’s id so an extra can’t borrow a character’s identity. The browser test was true and insufficient.

Approved art stays honest. TestEnvironmentChangeInvalidatesApprovedArt swaps the set under an approved panel and asserts it drops back to layout review with its history kept. TestImportedRenderRequiresApprovedCurrent3D refuses a render imported against unapproved or outdated geometry.

And some things stay human. The first automated capture of episode 1 succeeded in every technical sense, and one insert camera sat inside a character. No test flagged it. I did, by looking.

Dollars per accepted panel

Here is the part that stings, given what I do for a living.

The provider evaluation names the right metric: “dollars per accepted panel,” never dollars per image, because “an arm that costs triple per image and halves the reroll rate wins.” Then the handoff, item 6 of next steps: “Nothing stores provider, model revision, or spend, so cost per accepted panel … cannot be computed.” The parenthetical it skips calls that “the number that actually matters.”

I’ve built attribution pipelines for $10–12M a quarter of AWS spend and across a fleet of roughly 700,000 servers. My own weekend pipeline has the right metric written down twice and no column to store it.

Horizontal bar chart of list price per image: GPT Image 2 $0.28, Nano Banana Pro $0.14, Nano Banana 2 $0.08, fal flux-general about $0.055 to $0.063 at 1024 by 1536, FLUX.2 about $0.047, Seedream 5.0 $0.03. flux-general is marked as accepting a hard structural control map.
Vendor list prices researched in September 2026, not measured. The options that accept a pose or depth map sit near the cheap end, so control isn't a quality-for-cost trade.

A 9x spread per image looks decisive until you divide by acceptance rate. The table is an illustration only. It uses list prices, assumes one image per attempt, and invents the acceptance rates, because nobody has measured them yet.

Acceptance rateSeedream 5.0 ($0.03)flux-general (~$0.06)Nano Banana Pro ($0.14)GPT Image 2 ($0.28)
100%$0.03$0.06$0.14$0.28
50%$0.06$0.12$0.28$0.56
25%$0.12$0.24$0.56$1.12
10%$0.30$0.60$1.40$2.80
36-panel episode at 25%$4.32$8.64$20.16$40.32

Read it diagonally. Seedream at 10% acceptance costs $0.30 per accepted panel. GPT Image 2 at 100% costs $0.28. The cheapest image isn’t the cheapest panel if you throw away nine of ten.

The fix is the dashboard I’d actually manage from. Put provider, model revision, request ID and billed amount on every candidate, and the rubric’s defect flags on every rejection. That yields two numbers: dollars per accepted panel, which says what quality costs, and the top rejection reasons, which say which gate to strengthen next.

What I’d carry into the next pipeline

  1. Write acceptance before you look. Actor inventories, identity locks, defect flags, a decision rule fixed in advance.
  2. Gate every stage and spend the free ones first. Blocking and outlines catch structure for zero dollars.
  3. Relax who may approve only after the record is honest. Named actors, automated approvals labeled as assertions, 409s on stale writes.
  4. When a delegated approval fails, fix the process, not just the verdict.
  5. Keep spend out of the toolbelt. Dry run by default, explicit confirm, no retries.
  6. Every defect you paid for once gets a failing test first, at the boundary where it broke, and every paid call records its cost from day one.

The pipeline can draw a panel in twelve seconds. It still can’t tell me what an accepted one costs. That’s the next gate.

Previously in this series: TendForm: managing a solo SaaS build and TrendVesting: validating production AI. If you’re putting generative output into production and need someone to design the gates, let’s talk.

quality-gatesgenerative-aipipelinesunit-economics

Cash-pay advisory
from $250/hr

Check fit