Spaces:
Sleeping
Sleeping
| 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." | |
| ) | |
| 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() |