The vawe blog
We build a framework where an agent writes one HTML page and the renderer turns it into a film. These posts say what broke on the way, what we measured, and what we changed because of it.
Architecture
How the engine is shaped: one HTML page in, one film out, and the formats and owners we removed on the way.
A CSS animation library for agents: the vawe moves site
After we deleted the JSON engine copy (127,750 lines) we built a CSS animation library: 70 moves at launch, 125 now, 44 easing pages and one markdown file each.
An AI agent harness that refuses only three things
Our AI agent harness advises on every check and refuses only for determinism, a missing waiver reason and an unapproved final. Why, and what the guards cost.
Duplicate code in a framework: one owner per rule
Two private copies of one Lab colour formula could differ by about 0.001. How we removed duplicate code from a video framework and gave each rule one owner.
Video loudness: our sound sat 9 dB too quiet
We deleted our loudness logic, mixed audio as written, and measured LUFS on every draft. The check showed our defaults sat 9 dB low: -28.8 LUFS against -20.
How to seek CSS animation, forward and backward
To seek CSS animation and Web Animations frame by frame, you must handle seek-back, cancelled animations, fonts and a running timeline. Four bugs we fixed.
Deterministic rendering: time is the seek, never the clock
How a 75-line virtual clock makes a web page that calls Math.random and Date.now render the same video twice, and the test that checks it on every run.
An HTML video format that fits on one screen
Our HTML video format is a web page with a duration meta and one data-world element per beat. Twelve checks, the judge and the studio all read it. Here it is.
Headless components and themes: no shared house look
A shared default style made every film look like one UI kit. We moved to headless components plus ten themes that change tokens and never a class name.
How to delete dead code: our 205,163-line commit
The most useful commit of our month added 65 lines and deleted 205,163. How to delete dead code safely: the order we used, and the restore tag we kept.
HTML to video beat our own JSON scene format
We built a 24-type JSON format for agents, then a blind A/B test showed plain HTML to video finished in 5.9 minutes against 10.75. So we deleted the format.
Agent experience
What an agent gets wrong on its own, and the commands, defaults and checks that make a weaker model succeed.
AI agent shell hooks: change the environment, not the rule
We added a hook that refused for loops and sed -i, and reverted it in 7 minutes. 443 of 5,520 agent Bash calls had failed. The fix was the environment.
An agent hook that names the next command
A hook reads the film in progress and prints the next command. The agent followed it from brief to board, until dev printed one line 13 times in a row.
Building an AI agent harness for the weaker model
A strong model hides harness gaps and a weaker one shows them. Six weaker-model films: median 13 drafts, median first judged total 45, and 0 of 6 PASS.
Cut video to the beat: a beat grid an agent can read
Cut video to the beat with a beat grid an agent can read. Our sound verb failed 8 of 19 calls with 'no output'. The cause was a one-second minimum on cues.
How an AI agent watches a video: frames, strips, velocity
How an AI agent watches a video: ours read 5 reference frames and 2 of 16 strips. Its flashes ran 0.20 s; the reference's ran 0.07 to 0.10. Then we fixed it.
How long should AGENTS.md be? We ratchet ours
We cut what our agents read before their first layer from 5,402 words to 2,239 in four days, then let it grow back to 4,602 in ten. A ratchet counts the cost.
One verb to land a git worktree branch on main
Landing a worktree branch took about six manual steps and agents skipped some. vawe land rebases, scans for big files, runs tests and pushes in one call.
Reading AI agent traces: 74% of a session was waiting
We read agent transcripts and run logs for three rounds. In one 110-minute session, 81.5 minutes went to final renders and polls, and 3 of 5 finals failed.
Video reference comparison: our agent never did it
Our film scored 62 and still looked wrong. The reference flashes ran 0.10 s, ours ran 0.20 s, and no tool in the agent loop knew the reference existed.
AI agent observability: one JSONL line per verb
We asked an agent how long each step took and got 18 minutes for a 6.2 minute run. A one-line-per-verb log now answers time, failures and draft progress.
Lint waivers that must name a place and a number
Our audit found 13 waivers in 8 of 36 film pages, 2 of them dead. A waiver reason must now say where and what was measured, or it waives nothing at all.
One CLI for AI agents: one binary, one verb per step
We gave agents one command with verbs instead of a Makefile and scripts. It grew from 9 to 42 verbs, each one added after a trace showed hand-rolled work.
Make an AI agent ask clarifying questions first
vawe new prints five questions and writes nothing until subject, message and show are answered. Why we stopped letting an AI agent guess a film brief.
Taste
Motion, type and colour: how we turned judgement about a good film into rules, references and checks.
Blind test of UI skills for AI agents: one combo won
UI skills for AI agents, tested blind: 11 single skills gave cluttered web heroes. Of 6 combos, one won both frame kinds and two were rejected for both.
Depth of field in the browser: a virtual lens for HTML
Depth of field in the browser, plus bloom and halation: we film a flat HTML page with one WebGL lens. A text-shadow is not a bloom. The source shrank 10x.
Photo to point cloud: seeded particles from an image
Photo to point cloud: we deleted photo parallax on 1 October and added seeded photo particles on 11 October. A Hubble photo becomes a lit cloud in 1.3 MB.
Verbalized sampling for agents: four ideas, one typicality score
Verbalized sampling for agents: a model writes the most typical idea first, so we ask for four ideas and a typicality score. No result yet, only a test plan.
Video transitions that carry the eye: twelve handoff families
Video transitions that carry the eye: 12 handoff families, 10 banned slideshow tells and a check that compares the plan with the film. No measured gain yet.
Background music for generated video: we banned the synth bed
Background music for generated video: a constant synth hum passed our dead-air check, so we banned it. The ban commit changed 44 files, +133 and -376 lines.
CSS easing curves need names: 44 pages of easing data
We replaced hand-typed cubic-bezier numbers with nine named CSS easing curves and 44 pages of velocity data. What the names fixed and what we could not measure.
AGENTS.md at 2,239 words: one digest instead of 98 rules
Our weaker agents skipped long taste docs. We cut AGENTS.md reading to 2,239 words with a one-page digest, then watched the number creep back to 4,602.
A motion design system: ten looks, 70 clips
We gave 70 demo clips one motion design system: a shared foundation, ten token sets and a contrast gate. The rule that mattered most: faint is never text.
Measurement
Judges, benchmarks and numbers: how we know a change made films better and not only different.
Acceptance criteria for AI agents: a scoreboard that lied
We gave an AI agent measurable acceptance criteria. One film sat at 7 of 15 rows green for 10 drafts, because the rows measured the agent's own tables.
LLM as a judge: ours starts at reject and keeps a ledger
Our film judge scored a silent film zero for being silent. Here is how we made an LLM as a judge fair: reference caps, a fix ledger and declared exemptions.
AI agent reward hacking: 18 of 36 films gamed one check
Agents turned our checks green with the same cheap trick, a slow camera drift. An audit of 36 films showed 18 used it, and our own advice had taught them how.
Data over rules: pruning AGENTS.md with measurements
Our own rulebook was the first thing the data told us to cut. AGENTS.md rules over 500 characters fell from 324 to 42, while the docs agents read still doubled.
Ground truth fixtures: how we graded our video tools
Ground truth fixtures for our video tools: 11 families with known cuts, flashes, pans and zooms, scored per measure. The bar was one frame at 30 fps, 0.0334 s.
Measuring motion graphics quality across 167 cuts
We stopped asking if a film was good and measured 167 cuts in 17 reference films. The ranges became four checks, and our early drafts fell outside all four.
Puppeteer default viewport: how 800x600 broke our renders
Our judge reported a shrinking box three times and we called it a misread. The Puppeteer default viewport (800x600) was resetting render tabs. Here is the fix.
Calibrating measurement tools: every number gets an error bar
Calibrating measurement tools: 13 of 14 measures passed on known-truth fixtures (start-time p90 error 8 ms). One failed at 304 ms, so spec says check by eye.
Speed
Render time, draft loops and the work we stopped doing twice.
How we sped up end-to-end tests 40x by deleting a file format
Our end-to-end run fell from about 175 s to 4.3 s after we deleted the JSON scene format it mostly tested. See what the new run checks and what it costs.
Limiting headless Chrome renders: slots and a reaper
With 10+ agents and 18 headless Chromes, load hit about 18. Two render slots, a lane budget and a Chrome reaper fixed it. A sixth lane adds under 10% speed.
Long-running commands for AI agents: make the wait cheap
Long-running commands for AI agents: a draft spent 357 of 427 s in line behind a final. A background ship, one blocking wait and a canvas path cut idle time.
x264 preset fast vs medium: 25% faster, 15% larger
x264 preset fast against medium: 25% faster encode, 15% larger file, 49 dB PSNR apart. Plus dither, a 10-bit AV1 web copy and a 2 MB budget for every clip.
Deterministic rendering in headless Chrome: same pixels twice
70 of 216 frames differed between two renders of one page. How we got deterministic rendering in headless Chrome: six sources of drift and six fixes.
Faster headless Chrome screenshots for draft video renders
JPEG at quality 92, GPU compositing, slices sized to the work and a warm browser made our draft renders cheap. GPU was 1.5x faster. Here is where it broke.
Puppeteer deviceScaleFactor: why our half-size draft lied
Our draft laid the page out at 960x540 and the final at 1920x1080, so px values moved. Puppeteer deviceScaleFactor cut the frame diff from 10.07 to 0.86.
Puppeteer hangs: a 60 s stall watchdog for headless renders
Two headless renders hung silently for 600 s and 400 s, then passed on retry. A 60 s watchdog now names the stuck step. The slowest real step took 0.38 s.
Resumable video rendering: stop losing a final to one slice
Resumable video rendering: a final died at 97% and left 3.7 GB of orphans. Slice markers, four attempts and a content key fixed it. One session lost 49 min.
Motion blur in the browser: spend subframes where it moves
Our motion blur in the browser picks 1 to 16 subframes per frame from measured travel. A 5 s film took 4,017 subframes at a 16 cap and 7,805 at a cap of 32.
Headless Chrome video rendering: 4 workers, 1 lock
Our four render workers shared one browser and took turns at a single queue. One headless Chrome per lane cut capture from 140 s to 47 s on a 20 s film.