Sole developer · 2026-07–present

imagen

A self-hosted generation platform, and a pipeline that turns one sentence into a short film

A personal summer side project, teaching the dog to talk.

Hey Bravo, tell people why they should hire your dad.

Prompt Bravo convincing people to hire me 42s

No AI company was paid for any of this. Open tools, a GPU in this house, and a control plane I built to tie them together.

What it is

A self-hosted image and video generation platform. A FastAPI control plane sits in front of a ComfyUI render engine running on a desktop GPU in my house, with three front ends (a CLI, a terminal UI, and a React web app) over one durable job and gallery model.

419 commits in about sixteen days. 43 REST endpoints, ten database migrations, and roughly a 2:1 ratio of test code to source. It runs on my Docker Swarm behind Traefik.

I built the whole stack rather than renting it, which is the entire point: no paid generation APIs, no third-party service holding the queue, and nothing leaving the house that I did not choose to send.

Hardware decoupling as a design decision

The render engine runs on a separate machine (an AMD RX 7900 XT over ROCm) with per-model VRAM budgets modeled against its roughly 19 GiB. For full-precision video and for training, it bursts to a rented cloud GPU.

The separation is deliberate. Powering off my workstation cannot kill the render queue. That is a small architectural decision that removes an entire class of “why did my ten-hour job die” from my life, and it is the kind of thing you only think to do if you are used to being the person who gets paged.

The economics follow the same logic. I pay for GPU hours, never per generation. Hosted APIs price by output, which makes iteration the expensive part; so you ration exactly the behaviour the work depends on. Renting the machine inverts it: once the box is running, exploration is free, and cost becomes a function of time rather than of how many times I changed my mind.

A hazard register the build enforces

ComfyUI’s wire protocol is not a stable contract; it changes underneath you.

So every failure mode I have actually hit is written into a 17-row hazard register, and each row binds four things together: a re-verifiable citation for where the behaviour came from, a fault-injection toggle in a fake server that reproduces it, a guard in the source that handles it, and a named test that proves the guard works.

Then the build checks itself. A test fails if any named hazard test goes missing. You cannot quietly delete the evidence that a hazard was ever real, which is the failure mode I care about, because that is how hard-won knowledge evaporates from a codebase six months later.

Alongside it, fifty numbered architecture decisions with three amendments, so the reasoning is recoverable rather than remembered.

Blind, pre-registered evaluation

Seventy-two images, judged blind, with the decision rule written down in advance so that it could not be adjusted afterward to fit whichever model I had started rooting for. A comparison script handles the blind and reveal relabeling and verifies the actual generation settings out of the image metadata, rather than trusting my notes.

That is the same idea as everything else I build: the integrity is structural. Pre-registering the rule makes self-deception unavailable instead of merely discouraged.

Prompt to film

The newest layer started after I saw talking-dog videos on social media and thought, how hard could that be. Turns out custom ControlNets and muzzle tracking for the talking effect are harder than they look when you are building the pipeline from scratch instead of calling a hosted service.

I type one sentence (a short movie about my dog Bravo’s summer up north) and get a film. Not in one shot, and that is the point. The models are already good; the interesting problem is the shape of the collaboration around them.

Most generation tooling treats a model as a slot machine: write a prompt, pull the lever, get one output, pull again. That wastes what the model is best at, which is knowing how to ask. So this runs the other way. An LLM takes the one-line description and does the part I am worse at: breaking it into shots, writing the prompts in the form the video model actually wants, proposing several readings of each beat. I choose. It assembles.

The model handles translation and variation; I handle taste and continuity. Neither of us does the other’s job, and the pure-automation failure mode (a technically correct film that is not the one you meant) largely disappears once a person is choosing between real options instead of re-rolling a prompt.

Current generators are MiniMax H3 for video and Z-Image for stills, both swappable, because in this field anything hard-coded has a shelf life measured in weeks. I run local and frontier models side by side on purpose: the gap between them is the most useful signal available about where this is all going.

The film at the top of this page

Three shots, one file. The Beg, The Walk-Up, The Close, at 14.375 seconds each: 345 frames, exactly on H3’s 17n+5 frame lattice. Raw shot time is 43.125s; two crossfades bring the assembled master to 42.833s, and the cut on this page ends a little earlier still, on a half-second fade.

It is its own production in the repo, and it owns nothing. Its token overlay and its reference list are both empty files, and a test asserts they stay empty, because a key appearing in either one is a real decision that should be made out loud. Every setting, light, camera and identity plate comes from one shared bible that three productions read. When a second production wanted the same home office, the setting moved into the shared library rather than being restated, and both other productions rebuilt byte-identical across the move. That is the claim about composition being side-effect free, checked rather than asserted.

The generated output is committed, and a test rebuilds it and compares byte for byte. A rebuild that changes the output when the input did not change is a bug, not a diff to accept.

Every shot was rendered locally at low quality first, then re-rendered on rented GPUs, for a total cost measured in cents.

The dog is a trained model, not a description

Bravo is never described in the prompts. He is a LoRA trained on photographs of him, two recipes at 3000 steps each, trained out of band on a rented GPU, because nothing in the repository trains anything and neither development nor CI ever touches one.

That adapter is what renders the identity plates: the stills that fix his face, coat and collar before a frame of video exists, which each shot then addresses as a reference rather than describing him again in words. Alongside them a five-second recording is the voice reference, used for timbre only, with none of its words or timing.

The trigger token the production composes its prompts around is asserted against the training config by a test, because a trigger that drifts silently renders a different dog and nothing downstream would catch it.

No LLM calls in the render service

There are no LLM SDK calls in the render service itself. It is an image pipeline, and the only vendor-facing code path is a deny-list that blocks third-party cloud nodes from entering a ComfyUI graph. So a downloaded workflow cannot quietly ship my prompts somewhere I did not agree to.

How the film gets made

  1. One sentence · what I type The whole human input: a single line describing the film I want. Everything after this is the model doing the part I am worse at.
  2. Questions · only if needed The model asks only when the sentence is genuinely ambiguous, rather than interrogating me on every run. Silence here means it had what it needed.
  3. Identity plates · Bravo LoRAs Stills rendered locally from the LoRAs trained on photographs of Bravo. These fix his face, coat and collar before any video exists, and every shot addresses them as references rather than describing him again in words.
  4. Shot briefs · refs + voice One document per shot, naming the plates as picture references and a five-second recording as the voice reference. The briefs are checked into the repository verbatim, because the exact wording is what makes a render work.
  5. Pod up · provision, tunnel A GPU is rented on demand. The run provisions it and opens a tunnel to it; there is no standing server and nothing to pay for between films.
  6. Weights · pulled on boot The pod pulls the model weights it needs at start-up rather than carrying a prebuilt image around, so swapping a generator is a config change and not a rebuild.
  7. Render · 345 frames a shot Each shot is generated at 14.375 seconds, 345 frames, sitting exactly on the video model’s 17n+5 frame lattice. Three shots assemble into one file with two crossfades.
  8. Pod down · cost in cents The pod is destroyed as soon as the render lands. Billing is GPU time, not generations, so iterating costs minutes rather than per-output fees.
A one-line description becomes shot briefs, identity plates rendered from a LoRA trained on the dog, and a video job on a GPU that is rented, provisioned, used, and destroyed inside a single run.