How this site works

Everything that happens between pressing the button and getting a video, and why each piece is built the way it is. This is more detailed than the usual "how it works" page, because every decision here came out of something going wrong first.

The pipeline

  1. Your photo is uploaded to the local server and stored in a temporary folder
  2. If you chose a portrait motion, OpenCV runs a face check
  3. The photo is forwarded to a local ComfyUI instance
  4. A text prompt plus your photo go into MiniMax H3, an image-to-video model
  5. The model produces 73 frames (3.04 seconds at 24fps)
  6. ffmpeg joins the clip to a reversed copy of itself and strips the audio track
  7. The result comes back to you, and the page draws a frame around it in CSS

All of this happens on one physical machine. Your photo does of course travel over the internet to our server — that is unavoidable for any web service — but it is never passed on to a third-party AI API; no other service is involved at any point.

Why 73 frames

MiniMax H3 requires a frame count of the form 17n + 5. Three seconds at 24fps is 72 frames, which isn't valid, so it rounds up to 73 frames — 3.04 seconds. After the ping-pong loop that becomes 146 frames, 6.08 seconds.

The prompt never mentions the camera

This is the least intuitive part. The model tends to slowly push in on the subject, and the obvious fix is to write "don't zoom", "locked camera", "static shot" into the prompt. We measured the background displacement:

What the prompt saysBackground displacement (lower is steadier)
No mention of the camera at all6.6
"static shot"20.6
A full locked-camera sentence39.2

The harder you push, the more it moves. The likely reason is that this vocabulary only ever appears alongside camera-movement descriptions in the training data, so mentioning it reminds the model that camera movement is a thing. The prompt now contains no camera, shot or zoom.

The frame isn't drawn by the model

Same lesson. Mention a picture frame in the prompt and the model paints a gold frame into the image — crooked, wobbling, corners that don't meet. The frame is now overlaid by the page in CSS, so it is always straight, and you can change its colour without re-running anything.

The subject noun has to match the photo

Portrait prompts say "The person…". If there is no person in the photo, the model does not ignore that word — it generates a person, and pushes the shot in on the face it just invented. This is not hypothetical; it happened to a real user, and we reproduced it on the first attempt with a licensed photo (clips on the failure cases page).

The fix was to split the motions into two sets. The animal set never uses the word person; it says "the animal" and ends with No people appear. A face detector on upload catches the mismatch before you spend forty seconds on it.

Why the ping-pong loop

Looping a 3-second clip directly gives a visible jump at the wrap point, because the last frame doesn't line up with the first. Appending a reversed copy means the seam is the same frame on both sides, so the loop is invisible — at the cost of directional motions (waving, calling) reading as a rewind on the way back.

Speed: the slow part isn't the computation

We timed a 3-second clip node by node over a websocket:

StageTime
Understanding the photo (text encoder)~17.7 s
Sampling (bringing it to life)~11 s
VAE decode (developing the frames)~11.4 s
Everything elseMoving model weights onto the GPU

The card is an RTX 5070 Ti with 16GB of VRAM, but the text encoder plus the main model exceed 30GB, so the weights are streamed in on every run. The bottleneck is data movement, not maths. Swapping the text encoder for a smaller quantisation (25.3GB → 14.6GB), dropping the sampler from 6 steps to 4 (the acceleration LoRA was designed for 4 in the first place) and removing an audio decode we never used brought a single run from 60–80 seconds (worst case 137) down to a steady 42–45 seconds. We compared output quality at a fixed random seed: no visible difference.

Queueing and cancelling

One graphics card means jobs are processed one at a time. The home page shows the queue length, and while you wait the progress bar tells you how many jobs are ahead of yours. When you cancel: if your job is still queued it is removed from the queue; if it is running, the server first confirms that the running job is actually yours and only then sends the interrupt — it will never kill someone else's job.

Losing your connection doesn't lose the job

Job state is written to the server's disk, not held only in your browser. Close the tab, reload, let your phone freeze the page in the background — it reattaches when you return. Even if the server itself restarts, it asks ComfyUI whether that job finished and collects the result.

What it's built with

Try it yourself