← Back to list

I built Z Image Studio: four modes of free AI image generation, in one private browser app

Text-to-Image, custom LoRA styles, ControlNet, and an upscaler that actually adds detail — Apple Silicon, NVIDIA, or free on Hugging Face…

Mayank Gupta · 2026-05-25 17:45 · 0 claps · 12.9 min read
#ai #stable-diffusion #z-image-turbo #lora #art
Open on Medium ↗
Wiki topics: FT · Fine-tuning & Adaptation MM · Multimodal & Generative Media AI · AI · General CUL · Culture & Media 👗 · Fashion

I built Z Image Studio: four modes of free AI image generation, in one private browser app

Text-to-Image, custom LoRA styles, ControlNet, and an upscaler that actually adds detail — Apple Silicon, NVIDIA, or free on Hugging Face. Nothing saved server-side.

TL;DR — Z Image Studio wraps the new Z-Image model (a large Base with negative prompts, and a fast Turbo) in one focused Gradio app. Four things in three tabs: prompt-to-image, hot-loaded LoRA style adapters, ControlNet (Canny / Depth / Pose) as a pseudo image-editor, and an AI upscaler that invents detail instead of interpolating pixels. No signup, no watermarks, and 100% private — your images and your uploaded LoRAs live only in your session and get wiped on refresh. It runs locally on MPS or CUDA, and free in the browser on Hugging Face ZeroGPU. Code is MIT.

One prompt, three styles. Same subject, same seed — the LoRA does the rest.

Why I built this

Most “free” AI image sites are free until they aren’t. You get three generations, then a watermark, then a paywall, then a terms-of-service line that quietly says your uploads train the next model. The interesting capabilities — style adapters, ControlNet, upscaling — are buried behind a subscription or scattered across five different tools that each want your email.

Z-Image is a genuinely good new open model, shipped in two flavors: a large Base that takes negative prompts, and a fast Turbo. I wanted a single place where I could exercise the whole surface — prompt to image, drop a style LoRA on top, use an image as a structural template, and upscale the result — without uploading anything to a server that keeps it. So I built one. It runs on my Mac over MPS, on an NVIDIA box over CUDA, and it deploys to a Hugging Face Space on ZeroGPU so anyone can use it free, in a browser, with no account.

The honest version of this build log is that the first generation underwhelmed me, a LoRA fixed it, the LoRA introduced its own problem, and the upscaler fixed that. That loop — capability, limitation, repair — is the actual story, so that’s how I’ll tell it.

What’s under the hood

Gradio shell → one DiffSynth ZImagePipeline. Both transformers (Base + Turbo) sit preloaded in a pool and get swapped per request; the encoder, VAE, and tokenizer are shared between them so nothing is duplicated. ControlNet conditions the Turbo path; the upscaler branches off Turbo; LoRA.safetensors are hot-loaded per-variant. Runtime band underneath: Apple Silicon (MPS), NVIDIA (CUDA), or HF ZeroGPU.

A few decisions worth naming, because they were where the time actually went:

  • One pipeline, two transformers in a pool. Base and Turbo are different transformers but share the text encoder, VAE, and tokenizer. Loading two full pipelines would waste memory I don’t have on a Mac. Instead there’s a single DiffSynthZImagePipeline and the request handler indexes into the model pool to pick Base or Turbo. Flip the radio in the UI and the form swaps its own defaults - Steps, CFG, and the negative-prompt field - to match what that model actually honors.
  • The duration estimator is the ZeroGPU contract. On Spaces, the generate method is wrapped with@spaces.GPU(duration=...) at module load. ZeroGPU hands you a GPU for a declared window, so I wrote an estimator that clamps to a 60-180 second budget based on mode, model, step count, and image area. Under-declare and the task gets killed mid-denoise; over-declare and you sit lower in the queue. If a generation does abort, the handler retries once at twice the duration rather than just failing in your face.
  • MPS needed babysitting. DiffSynth’s free-VRAM check is CUDA-only, so on Apple Silicon it’s bypassed — module swapping still works without it.PYTORCH_ENABLE_MPS_FALLBACK=1 is set automatically so the handful of ops MPS doesn't implement fall back to CPU instead of crashing. Cold-start pipeline construction is ~30-60s on MPS, but it's amortized across your whole session.
  • The cache mirror trick. A Space’spreload_from_hub pulls the ~47 GB weight set at build time into a read-only cache. On first request,app.py mirrors that into a writable directory and symlinks each snapshot into the./models/<repo>/ layout DiffSynth expects. Pipeline construction then finds everything locally - no network on inference after the first.
  • Restraint as a UI principle. The theme is “Soft Dark Restraint”: a warm dark substrate, cream text, and exactly one accent color rationed across the live radio dot, slider fill, the primary button, and the progress bar. No shadows, no gradients, no display fonts. Controls stay hidden until you ask for them — the negative-prompt and CFG fields only appear when you pick Base, the LoRA slot only when you tick the box. The accent is rationed so the thing you’re actually looking at — the generated image — stays the focus.

App code is MIT. It stands on DiffSynth-Studio (Apache-2.0) for the pipeline and model pool, Z-Image and Z-Image-Turbo from Tongyi-MAI, the Fun-Controlnet-Union 2.1 adapter from Alibaba PAI, and RealESRGAN weights for the upscaler. None of that is mine; the studio is the glue.

A tour of the four modes

Four capabilities, three tabs. Same model knobs everywhere — model, steps, seed, optional LoRA — and each mode adds only what it specifically needs.

Same screen, same core knobs everywhere — Base/Turbo, steps, seed, and an optional LoRA toggle. Each mode adds what it specifically needs: a negative prompt for Base, a control image and preprocessor for ControlNet, a refine-strength slider for upscale.

1 · Text-to-Image

This is the front door, and it’s where the honesty starts.

The Text-to-Image tab. Base = 25 steps, cfg 4, negative prompt enabled. Turbo = 8 steps, cfg locked at 1, no negative prompt. “Edit” and “Omni Base” are stubbed as coming-soon links to the official Z-Image repo — I’ll wire them up when they ship.

I started on Base with a deliberately simple prompt — “A child playing with forest spirit, spirit looks like a masked woman, translucent body” — and a negative prompt of blurry, lowres, lack of detail. Hit generate.

Base, 25 steps, cfg 4. Technically clean — photoreal forest, correct composition — but the masked spirit reads more eerie than enchanted. Cool, not magical. That gap is the whole reason the next section exists.

It’s a competent image. The forest is convincing, the composition is right, the spirit is literally a masked woman with a translucent body like I asked. But it’s cool, not magical — it looks like a photo of two cosplayers, not a storybook. That’s not a model failure; it’s me asking a general-purpose model for a specific aesthetic and getting the literal-minded answer.

Before changing anything, I noted the seed. This matters more than it sounds. With seed = 0 the app randomizes every run, so two generations of the same prompt give you two different images - useless for comparison. Pin the seed to a fixed number and you get the exact same image back, every time. That's the only way to A/B a prompt or a LoRA cleanly: hold the seed constant so the only variable that moves is the one you're testing.

Turbo, 8 steps. The Steps tooltip says it plainly — “Turbo: 6–10. Base: 20–30.” Turbo trades the negative prompt and CFG for speed, and on a Mac that’s the difference between ~30–60s and ~2 minutes.

2 · LoRA adapters

This is the differentiator, and the moment the studio stops being a toy.

A LoRA is a small adapter you bolt onto an existing model to change its behavior — same Z-Image underneath, retrained on a small, labeled dataset until it learns a specific look. I took the exact same prompt and the exact same seed from the Base baseline, ticked “Use a LoRA,” and loaded a Dark Ghibli Fairytales adapter.

Same prompt, same seed, one LoRA. The photo became a painting — soft light through the canopy, a hand-illustrated forest spirit, real storybook warmth. This is the “now it’s magical” beat.

Same scene. Same seed. Completely transformed. The photoreal cosplay became a dreamy, painterly fairytale — exactly the aesthetic the bare prompt couldn’t reach. And because I held the seed, I know the LoRA did it and not some lucky reroll.

The receipts. z-base-ghibli.safetensors at strength 0.8, and the Meta panel logging mode t2i, model Base, 25 steps, cfg 4, seed 285607310. Every generation prints its full config so you can reproduce it later.

The lever that mattered here is the per-variant rule: a Base LoRA and a Turbo LoRA are not interchangeable. The toggle label tells you which model’s adapter it’ll accept and updates as you flip the radio. So to show Turbo’s range, I switched models and loaded a Mythic Fantasy LoRA built for Turbo instead.

Mythic Fantasy LoRA on Turbo, 8 steps. Crowned armored kings, a glowing orb, a bioluminescent forest — convincingly fantasy. Look closely at the faces and armor, though: at this speed tier the fine detail is soft. Hold that thought for the upscaler.

What a LoRA actually is, end to end. A checkpoint (.safetensors) is just a snapshot saved every N training steps - show the model your 50 images 3000 times, save at 200, 400, 600 iterations, keep the one that looks right.

The reason there’s a LoRA upload slot and not just a fixed menu of styles is the killer use case: train one on your own face. Feed a few dozen photos of yourself in different clothes and lighting, label them, train, and you get a.safetensors that drops you into any scene the model can imagine - "Mayank standing behind a tree, overlooking a spaceship," rendered with my actual facial features. That only works if you can bring your own checkpoint.

Which raises the obvious worry, so let me kill it directly: your uploaded LoRA is transient. It lives in your session, it’s wiped when you refresh, and nobody else can see or download it. Same for every image you generate. Nothing is persisted server-side. If you train a face LoRA, it does not leak.

3 · Upscale

Remember the soft faces on the fantasy kings. This mode is the fix.

The Upscale tab. Defaults are sane out of the box — refine prompt “masterpiece, 8k”, 5 refine steps, denoise 0.33. Drop an image, hit generate.

I uploaded the fantasy-kings image and ran it with defaults. At a glance the before and after look similar — until you put them side by side.

Left: original. Right: upscaled. The composition is identical; the difference is everything underneath it — armor engraving, fabric weave, the texture of the magic.

The most dramatic save came later, in the Pose demo. In the raw Turbo output the near child’s face was a flat smudge — you could barely call it a face. Same upscale pass, denoise 0.33, and it reconstructed actual features: eyes, nose, individual hair strands. The model invented plausible detail from context; it didn’t interpolate pixels. (It quietly fixed a mangled leg tattoo too — but that’s getting ahead of the ControlNet section.)

Here’s the lever, and the thing most people get wrong about “upscaling”: this is not algorithmic. It isn’t drawing lines between pixels or smoothing gradients. The pipeline runs RealESRGAN ×4 to lift resolution, then does a short Z-Image-Turbo img2img refinement pass — 5 steps at denoise 0.33 — that invents plausible detail where the original had none. Zoom into the eyes on the before image and they’re a soft smear; zoom into the after and there are actual irises. The model hallucinated them, sensibly, from context. That’s why it uses Turbo under the hood, and it’s the natural repair step for anything that came out a little soft — which, at 8 steps, Turbo often does.

4 · ControlNet

The last mode is the one that turns the studio into a pseudo image-editor. You give it a control image, it extracts structure, and it regenerates a new image that obeys that structure.

The ControlNet tab runs Turbo + Fun-Controlnet-Union 2.1. Drop a control image and the preprocessor preview shows you exactly what the model will see — the edge map, depth map, or pose skeleton — live, as you switch preprocessors. The ControlNet-scale slider sets how hard that structure binds the output (1.0 is the default; lower lets the prompt drift, higher locks to the source).

Canny detects edges — it outlines every boundary in the image, then regenerates guided by those lines. I fed it the fantasy-kings scene plus a Fantasy LoRA and a Joker-flavored prompt:

Canny edges from the kings scene + a Fantasy LoRA. It kept the two-figure composition and stance from the source edges but re-skinned everything into ornate red-robed, Black-Myth-Wukong-looking figures. The structure is borrowed; the content is invented.

Then the realistic case. I uploaded a photo of a bedroom and gave it a one-line prompt — “night time scene”:

Canny on a real bedroom photo, ControlNet scale 1. The edge map preserves the room’s geometry — bed, shelving, windows — and the prompt relights it for night. Honest note: it’s a bit soft and invents some furniture, but that’s a job for the upscaler and the scale slider.

Depth reads near-versus-far structure instead of edges, so you keep the spatial layout but not the exact outlines. I pointed it at a forest scene as the depth template and prompted “kids playing, city in background”:

Depth-guided regeneration. It held the two-figure spatial arrangement from the control image but rebuilt the scene as kids in a city. It also got funny — the model filled the figures with bizarre colorful costumes, because the prompt left room and depth doesn’t constrain texture. Residual softness, again, is upscaler territory.

Pose detects the skeleton of the subjects and regenerates a brand-new subject in the same pose:

Pose detection via OpenPose. The skeleton captures the two-figure handshake stance; the output rebuilds completely different subjects — two kids on a street — holding that exact pose. The faces came out rough at this tier, and a leg tattoo looked off — both of which the upscaler cleaned up on a follow-up pass.

The lever across all three: ControlNet borrows structure and invents content, and how strictly it borrows is the scale slider’s job. It pairs naturally with the upscaler — ControlNet gets you the right composition fast on Turbo, the upscaler fixes the detail it left soft. The two-more-modes I haven’t built yet — Z-Image Edit and Z-Image Omni Base — are stubbed in the UI as coming-soon links to the official repo; the moment Tongyi-MAI ships them, they go in the same studio.

Run it yourself

Local install:

git clone https://github.com/techfreakworm/z-image-studio cd z-image-studio bash setup.sh # creates .venv, installs requirements source .venv/bin/activate python app.py # http://127.0.0.1:7860

First run resolves the weight set into your Hugging Face cache (~/.cache/huggingface/hub/) - budget time and bandwidth for roughly 47 GB across Z-Image, Z-Image-Turbo, the ControlNet adapter, and RealESRGAN. Subsequent starts are fast: the app symlinks the cache snapshots into DiffSynth's expected layout, so nothing re-downloads. The very first inference of a session is slow while the pipeline constructs (~30-60s on MPS, ~10-20s on CUDA); everything after that hits a warm cache.

Hardware floor: Python 3.11, ~50 GB free disk for the weights, and roughly 24 GB VRAM (CUDA) or 32 GB unified memory (Apple Silicon). On a Mac, Turbo at 8 steps and 102⁴² lands in ~30–60s; Base at 25 steps is closer to two minutes; the upscale refinement pass adds ~30s.

No hardware? Use the free Hugging Face Space — it runs on ZeroGPU and needs nothing but a browser. The one thing to respect there is the duration cap: the estimator clamps at 180s, so stick to Turbo, keep steps in the 6–10 range, and don’t push the image area past ~204⁸² total, or a long Base run can abort mid-generation before it finishes. (If it does abort, the handler retries once at double the budget.)

Why open source

The practical case is simple. Free image sites change their terms, throttle you, or start training on your uploads — and you find out after the fact. Running the weights yourself means the cost is your own electricity, the rate limit is your own patience, and the privacy guarantee is architectural, not a promise in a footer: the images and LoRAs in this app are transient by design, wiped on refresh, never written to a server. If you want a different model, swap it. If you want Z-Image Edit the day it lands, the code is right there and you can wire it in before I do. MIT means you can fork it, ship it inside your own product, or just read it to learn how the ZeroGPU duration dance actually works. That’s the whole pitch — not ideology, just control.

The two LoRAs from the walkthrough are both on Civitai — Dark Ghibli Fairytales (for Z-Image Base) and Velvet’s Mythic Fantasy Styles (for Z-Image Turbo). If the Space is useful, drop a ♥ on it. If the repo is useful, star it.

Built by Mayank Gupta · @techfreakworm on Hugging Face and GitHub ·mayankgupta.in


메타데이터
post_id
fcbb645bf3b6
slug
i-built-z-image-studio-four-modes-of-free-ai-image-generation-in-one-private-browser-app-fcbb645bf3b6
url
https://medium.com/@techfreakworm/i-built-z-image-studio-four-modes-of-free-ai-image-generation-in-one-private-browser-app-fcbb645bf3b6
canonical_url
https://medium.com/@techfreakworm/i-built-z-image-studio-four-modes-of-free-ai-image-generation-in-one-private-browser-app-fcbb645bf3b6
author_url
https://medium.com/@techfreakworm
status
ok
fetched_at
2026-06-09 15:37:30