Z-Image-Turbo2 / app.py
hmb's picture
hmb HF Staff
Update app.py
4816ffd verified
Raw
History Blame Contribute Delete
6.39 kB
import os
import tempfile
import spaces
import torch
import gradio as gr
from diffusers import DiffusionPipeline
# Load the pipeline once at startup. The Space is a ZeroGPU space, so the
# model weights stay resident and `@spaces.GPU` allocates a worker per call.
print("Loading Z-Image-Turbo pipeline...")
pipe = DiffusionPipeline.from_pretrained(
"Tongyi-MAI/Z-Image-Turbo",
torch_dtype=torch.bfloat16,
low_cpu_mem_usage=False,
)
pipe.to("cuda")
print("Pipeline loaded!")
def _save_image(image) -> dict:
"""Mirror of `gradio.workflow._save_tmp`: serialize a PIL.Image as a JSON
pointer the canvas can render. `Workflow.launch()` already adds the
tempdir to `allowed_paths`, so the /gradio_api/file=… URL resolves."""
path = os.path.join(
tempfile.gettempdir(), f"zimage_{os.urandom(8).hex()}.png"
)
image.save(path)
return {
"path": path,
"url": f"/gradio_api/file={path}",
"orig_name": "zimage.png",
"mime_type": "image/png",
}
def _estimate_duration(prompt, height, width, num_inference_steps, seed, randomize_seed) -> int:
"""Rough wall-clock estimate (seconds) for one Z-Image-Turbo call.
ZeroGPU's default per-call duration is 60s. Requesting less than you need
*raises* queue priority (shorter tasks get scheduled sooner) and — crucially
for a busy shared Space — frees the GPU slot for the next visitor far
sooner than holding it for a full minute, so far fewer users hit the
Space's "reached its GPU limit" rejection. Scaled by pixel count and steps;
clamped to a small floor/ceiling so a runaway slider can't starve the queue
or under-budget a big call.
Signature mirrors the GPU function exactly because @spaces.GPU passes the
decorated function's inputs straight through to the duration callable.
See: https://huggingface.co/docs/hub/en/spaces-zerogpu#duration-management
"""
pixels = max(int(height), 1) * max(int(width), 1)
# ~0.4s/step at 1024^2, linear-ish in pixels. Big calls still need headroom.
per_step = 0.4 * (pixels / (1024 * 1024))
seconds = int(num_inference_steps) * per_step
return max(20, min(int(seconds) + 15, 120))
def _friendly_gpu_error(err: Exception) -> str:
"""Turn ZeroGPU's terse allocator rejections into a clear, honest message.
'Space app has reached its GPU limit' is a *Space-level* capacity rejection
(the shared ZeroGPU pool is saturated), not a per-user quota wall — it
reproduces regardless of inputs, account tier, or sign-in state. Don't make
an upgrade claim whose truth we can't pin down, so the message is neutral:
shared GPU at capacity, retry shortly.
"""
msg = (str(err) or "").lower()
capacity_hints = (
"gpu limit", "reached its gpu limit", "gpu quota", "out of quota",
"quota", "no gpu", "could not allocate", "gpu is busy", "too many",
"concurrent",
)
if any(h in msg for h in capacity_hints):
return (
"⛔ This demo's shared GPU is at capacity right now — it's not a "
"problem with your prompt or your account. The GPU pool is fully "
"booked by other users at the moment. Please wait a minute and "
"retry; demand clears between bursts."
)
if "out of memory" in msg or "oom" in msg or "cuda" in msg:
return (
"💥 Image generation ran out of GPU memory. Try a smaller "
"Height/Width or fewer Inference Steps, then retry."
)
return (
"⚠️ Image generation failed. Please try again in a moment — if it "
"keeps happening, simplify your prompt or lower the resolution."
)
@spaces.GPU(duration=_estimate_duration)
def _generate_image_gpu(
prompt: str,
height: int,
width: int,
num_inference_steps: int,
seed: int,
randomize_seed: bool,
):
"""The GPU-decorated worker. Runs only under a ZeroGPU allocation; the
allocator raises *before* this body if no GPU can be granted, which is why
the rewording lives in the plain `generate_image` wrapper below, not here.
"""
if not prompt or not prompt.strip():
raise gr.Error("Please enter a prompt.")
if randomize_seed:
seed = torch.randint(0, 2**32 - 1, (1,)).item()
generator = torch.Generator("cuda").manual_seed(int(seed))
image = pipe(
prompt=prompt,
height=int(height),
width=int(width),
num_inference_steps=int(num_inference_steps),
guidance_scale=0.0,
generator=generator,
).images[0]
return _save_image(image), int(seed)
def generate_image(
prompt: str,
height: int,
width: int,
num_inference_steps: int,
seed: int,
randomize_seed: bool,
):
"""Workflow-facing wrapper around the GPU worker.
Bound to the canvas as a `fn` operator node — the workflow calls this
Python function directly server-side, so the entire pipeline (frontend +
ZeroGPU) lives in a single Space. This non-GPU wrapper catches rejections
from the `@spaces.GPU` allocator (which fire before the worker body runs)
and rewords them into a clear, honest user-facing message.
Returns (image_dict, seed_used). The image is serialized to a /gradio_api
file URL so JSON serialization across the fn bridge succeeds; the
executor's `fromGradioOutput` turns the dict back into an image port value.
"""
try:
return _generate_image_gpu(
prompt, height, width, num_inference_steps, seed, randomize_seed
)
except gr.Error:
# Already a user-facing validation message (e.g. empty prompt) — pass
# it through unchanged.
raise
except Exception as e:
# Allocator rejection (GPU limit / quota / OOM / etc.) — reword.
raise gr.Error(_friendly_gpu_error(e)) from e
# The workflow (workflow.json) wires `generate_image` as a `fn` operator:
# Prompt, Height, Width, Inference Steps, Seed, Randomize Seed ─▶
# generate_image (fn operator, kind="fn") ─▶ Output Image, Seed Used
#
# On a Space with `hf_oauth: true`, visiting the canvas runs this function
# under a ZeroGPU worker using each visitor's own HF token.
demo = gr.Workflow(
graph="workflow.json",
bind={"generate_image": generate_image},
)
if __name__ == "__main__":
demo.launch()