A generative studio that runs on your own Modal account. Train a LoRA on your photographs, generate stills with it, and animate any of them into a clip — in one interface, on one URL, with nothing to keep alive between sessions.
It is a real interface, not a form in front of a script. The canvas is the largest thing on screen at every moment, because the picture is the reason the page exists. Everything you can change lives in a bar under it — never a rail beside it, which would cost the image 384px of the one dimension it cannot get back.
Image and video are one workspace. They share the prompt, the canvas and the gallery. The switch is a chip inside the prompt field, and the sentence survives it — because a shot you described as a still is the same sentence you would describe as a clip. There is no mode to navigate to and nothing to retype.
The controls follow the model. Wan 2.2 takes LoRAs, a negative prompt and CFG; MiniMax-H3 is guidance-distilled and carries its own soundtrack, so it offers none of those and offers references instead. Only the controls the chosen model actually reads are on screen — a control that is present but ignored is worse than one that is absent.
LoRAs are written in the prompt. <lora:my_style:0.8>, the syntax anyone who
has trained these models already types. Strength defaults to 1 and the token
sits where the LoRA applies, so a fifth LoRA costs the canvas nothing — the rows
this replaced cost 380px of it for four filenames. + LoRA still opens a
picker, because you cannot type a syntax you have never seen.
Copy is a last resort — but a number is not a value it can show. Design first, then an icon, then words. A control that shows its own value gets no label; the two keyframe tiles put the mark where the frame sits in the clip rather than captioning themselves "first" and "last". Hyperparameters are the exception, and they are the exception on purpose: "32" is a rank, an alpha, an epoch count or a seed with equal plausibility, so every numeric field carries its name and the tooltip says what the number does.
Your work stays beside your work. The gallery is a drawer next to the canvas, not a destination you leave the studio to visit, because the still you made an hour ago is raw material for the clip you are making now. Any image can go straight back to the prompt, or become the first frame of a video, without a download and a re-upload. Open it full-width when you want the whole room.
Datasets are for reading, not just uploading. Captions are written in prose by Qwen3-VL-8B-Instruct, because the text encoders these models use parse grammar — "red dress, blue jacket" cannot say which garment is which, and a sentence can. The panel beside the contact sheet reads the set back to you: trigger-word coverage, caption length, duplicates, and the clauses your captions repeat, so you can see what the LoRA is about to learn by accident.
pip install modal
modal setup
modal deploy app.pyThat is the whole install. The last command prints a URL, and the URL is the application: interface, API and GPU jobs. Nothing runs on your machine, nothing runs when you are not using it, and there is no config file to fill in first.
Train. LoRA training for Krea 2 on musubi-tuner.
Point it at a folder of images, get a .safetensors back.
Caption. Datasets are named folders of images with .txt sidecars beside
them, which is exactly what the trainer reads.
Generate stills. Krea 2 inference on a vendored sd-webui-forge-classic backend, with LoRA stacking and regional prompting.
Generate video. Two families through a driven ComfyUI:
| MiniMax-H3 | Wan 2.2 | |
|---|---|---|
| Audio | yes, same latent | silent |
| CFG / negative | no — guidance-distilled | yes |
| LoRAs | no | yes |
| References | ref2va checkpoint | no |
| Experts | one | two on A14B, one on the 5B |
Adding Wan did not add a backend. It reuses the container, the warm ComfyUI process and the job contract; what is per-family is a graph builder and one row of capabilities — which is also the row the composer reads to decide what to show you.
- A Modal account.
modal setupwalks you through auth in a browser. - Python 3.10+ locally, only to run the
modalCLI. - A HuggingFace account if you want Krea 2 — its weights are gated. Everything else downloads without one.
You do not need a local GPU, Docker, a .env file, or any Modal Secret. The
HuggingFace token is pasted into the UI and stored in a Modal Dict.
Nothing downloads on its own. A fresh deployment has an empty volume and every model is opt-in, because the full catalogue is ~206 GB and almost nobody wants all of it. Open the deployed URL, click the gear, and pick what you need.
| Family | Size | Gated | What it buys |
|---|---|---|---|
| Krea 2 — images | 62 GB | yes | training + still generation |
| MiniMax-H3 — video | 64 GB | no | video with a soundtrack, references |
| Wan 2.2 — video | 76 GB | no | silent video, CFG, LoRA support |
| Wan 2.2 speed LoRAs | 5 GB | no | fewer steps per clip |
You do not need a whole family. The smallest useful video setup is Wan 2.2 TI2V 5B at 18 GB — the 5B checkpoint, umT5-XXL and the 2.2 VAE — which does both text-to-video and image-to-video on its own.
Downloads run on CPU containers, never on a GPU. Pulling 26 GB while an A100 idles is money burned for nothing.
A transfer reports the bytes it has and the rate it is getting them at, and if it goes quiet for four minutes it is abandoned and resumed from where it stopped, up to five times. Both exist because of one failure: a 17 GB pull stopping dead at 4 GB and the job staying "running" — no error, no log line, no byte count — until the four-hour timeout collected it. A download that can hang is survivable; one that can hang silently costs you the four hours before you learn anything.
Krea 2 RAW and Krea 2 Turbo need a HuggingFace token, and you must accept the licence with the same account that issued it:
Paste the token under the gear. If the licence has not been accepted, the error says so and links the page rather than failing as a generic 403.
Most LoRAs worth having were never published to HuggingFace — they are a link
someone sent you. Paste one under the gear and it lands in loras/, ready to
name in a prompt.
- A file link or a bare id both work; so does a folder link.
- Only
.safetensorsis kept. A folder's preview grid and readme are named as skipped rather than quietly copied onto the volume. - Leave folder blank and the files drop in loose, each its own entry. Give
one and they are grouped as versions of a single LoRA under
loras/{folder}/— which is right for a matched pair and wrong for a bag of unrelated ones, so it stays your call. - The link has to be shared with anyone who has it. Drive answers an unshared file with a sign-in page rather than an error, so the failure names that case explicitly instead of surfacing a parse error.
One Modal Volume, mounted at /workspace:
models/ weights, flat, addressed by exact filename
loras/ trained LoRAs, one folder each; loose files work too
datasets/{name}/ images + .txt caption sidecars
outputs/{job}/ generated media + a visionary.json sidecar
work/, .cache/ disposable
The layout is the contract, not the code. Datasets are folders of images with text files beside them — the same thing the trainer reads — so nothing here is required to get your data back out.
Run a second, isolated copy against its own storage by setting the volume name:
VISIONARY_VOLUME=visionary-test modal deploy app.pyEach job type picks its own class, and most are switchable in the UI:
| Job | Default | Options |
|---|---|---|
| Training | A100-40GB | — |
| Captioning | A100-40GB | — |
| Image generation | A100-40GB | A100-40GB, A100-80GB, H100 |
| Video generation | H100 | H100, H200 |
Containers stay warm between requests (10 minutes for images, 15 for video) so consecutive takes skip the model load, then scale to zero. You are billed for GPU time while a job runs and while a container is warm — not for the deployment sitting idle.
Anything that can fail cheaply does. A bad LoRA path, an unknown aspect ratio or a missing weight is rejected on CPU in milliseconds, before a GPU is rented.
Two smoke tests, both cheap, both runnable against your own account:
modal run tools/smoke_video.pyChecks every video graph — all twelve variants across both families — against the real ComfyUI node schema on a CPU container with no weights present. Catches a renamed node, a moved input, a dangling link, and a sampler the UI offers that ComfyUI does not have. It does not run a sampler, so it says nothing about whether a clip looks right.
modal run tools/smoke_krea2.py --gpu --lora anyExercises the Krea 2 loader, the LoRA stack and the regional prompting path.
Being honest about coverage, since "it deploys" is not "it works":
- Wan 2.2 TI2V 5B — text-to-video and image-to-video both verified on an H100, output inspected frame by frame.
- MiniMax-H3 — graphs validate structurally; no full run yet, so the audio path is unproven.
- Wan 2.2 A14B — graphs validate structurally. The two-expert handover cannot be checked structurally: wrong noise flags give a washed-out clip rather than an error, so only a real run will show it.
The front end is one self-contained string in app.py with no build step, so
it can be served locally against stubbed JSON instead of paying an image build
and a cold start per CSS change:
python3 tools/preview_ui.py 8777The stubs are shaped to hold the awkward states — a missing model, an uncaptioned dataset, a prompt too long to belong in a gallery card.
app.py the whole application — images, jobs, API, and the UI
forge/ vendored sd-webui-forge-classic backend (see forge/VENDOR.md)
tools/ smoke tests and the local UI preview server
CLAUDE.md the design rationale — why the code is shaped the way it is
app.py is deliberately one file. It is long, but the alternative — a package
whose modules are imported by Modal image builds — trades one long file for a
build-order problem, and the file is navigable by its banner comments.
If you are going to change anything, read CLAUDE.md first. It explains the
tradeoffs the code is holding, including several that look like mistakes until
you know what they are avoiding.
AGPL-3.0. Not a preference — an inheritance, and worth understanding before you fork this or run it for anyone but yourself.
forge/ is a vendored slice of
sd-webui-forge-classic,
which is AGPL-3.0, and it is imported and executed as part of the image
generation path rather than sitting there unused. AGPL-3.0 is strong copyleft
with a network-use clause: section 13 means that if you modify this and let
other people use it over a network, you owe those users the corresponding
source — deploying rather than distributing is not the loophole it is under
the GPL. forge/modules_forge/packages/comfy/ carries GPL-3.0 on top of that.
Since this deploys as a web application by design, that clause is the normal case here, not an edge one. Running your own private instance triggers nothing.
forge/VENDOR.md records the exact upstream commit and every local change, so
a sync is a diff rather than an archaeology exercise.
Model weights carry their own separate licences — Krea 2's in particular is gated and has terms you accept on HuggingFace. Nothing here grants you rights to them.



