Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -34,8 +34,64 @@ def _save_image(image) -> dict:
|
|
| 34 |
}
|
| 35 |
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
prompt: str,
|
| 40 |
height: int,
|
| 41 |
width: int,
|
|
@@ -43,15 +99,9 @@ def generate_image(
|
|
| 43 |
seed: int,
|
| 44 |
randomize_seed: bool,
|
| 45 |
):
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
calls this Python function directly server-side, so the entire pipeline
|
| 50 |
-
(frontend + ZeroGPU) lives in a single Space.
|
| 51 |
-
|
| 52 |
-
Returns (image_dict, seed_used). The image is serialized to a /gradio_api
|
| 53 |
-
file URL so JSON serialization across the fn bridge succeeds; the executor's
|
| 54 |
-
`fromGradioOutput` turns the dict back into an image port value.
|
| 55 |
"""
|
| 56 |
if not prompt or not prompt.strip():
|
| 57 |
raise gr.Error("Please enter a prompt.")
|
|
@@ -72,7 +122,40 @@ def generate_image(
|
|
| 72 |
return _save_image(image), int(seed)
|
| 73 |
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
# Prompt, Height, Width, Inference Steps, Seed, Randomize Seed ─▶
|
| 77 |
# generate_image (fn operator, kind="fn") ─▶ Output Image, Seed Used
|
| 78 |
#
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
|
| 37 |
+
def _estimate_duration(prompt, height, width, num_inference_steps, seed, randomize_seed) -> int:
|
| 38 |
+
"""Rough wall-clock estimate (seconds) for one Z-Image-Turbo call.
|
| 39 |
+
|
| 40 |
+
ZeroGPU's default per-call duration is 60s. Requesting less than you need
|
| 41 |
+
*raises* queue priority (shorter tasks get scheduled sooner) and — crucially
|
| 42 |
+
for a busy shared Space — frees the GPU slot for the next visitor far
|
| 43 |
+
sooner than holding it for a full minute, so far fewer users hit the
|
| 44 |
+
Space's "reached its GPU limit" rejection. Scaled by pixel count and steps;
|
| 45 |
+
clamped to a small floor/ceiling so a runaway slider can't starve the queue
|
| 46 |
+
or under-budget a big call.
|
| 47 |
+
|
| 48 |
+
Signature mirrors the GPU function exactly because @spaces.GPU passes the
|
| 49 |
+
decorated function's inputs straight through to the duration callable.
|
| 50 |
+
|
| 51 |
+
See: https://huggingface.co/docs/hub/en/spaces-zerogpu#duration-management
|
| 52 |
+
"""
|
| 53 |
+
pixels = max(int(height), 1) * max(int(width), 1)
|
| 54 |
+
# ~0.4s/step at 1024^2, linear-ish in pixels. Big calls still need headroom.
|
| 55 |
+
per_step = 0.4 * (pixels / (1024 * 1024))
|
| 56 |
+
seconds = int(num_inference_steps) * per_step
|
| 57 |
+
return max(20, min(int(seconds) + 15, 120))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _friendly_gpu_error(err: Exception) -> str:
|
| 61 |
+
"""Turn ZeroGPU's terse allocator rejections into a clear, honest message.
|
| 62 |
+
|
| 63 |
+
'Space app has reached its GPU limit' is a *Space-level* capacity rejection
|
| 64 |
+
(the shared ZeroGPU pool is saturated), not a per-user quota wall — it
|
| 65 |
+
reproduces regardless of inputs, account tier, or sign-in state. Don't make
|
| 66 |
+
an upgrade claim whose truth we can't pin down, so the message is neutral:
|
| 67 |
+
shared GPU at capacity, retry shortly.
|
| 68 |
+
"""
|
| 69 |
+
msg = (str(err) or "").lower()
|
| 70 |
+
capacity_hints = (
|
| 71 |
+
"gpu limit", "reached its gpu limit", "gpu quota", "out of quota",
|
| 72 |
+
"quota", "no gpu", "could not allocate", "gpu is busy", "too many",
|
| 73 |
+
"concurrent",
|
| 74 |
+
)
|
| 75 |
+
if any(h in msg for h in capacity_hints):
|
| 76 |
+
return (
|
| 77 |
+
"⛔ This demo's shared GPU is at capacity right now — it's not a "
|
| 78 |
+
"problem with your prompt or your account. The GPU pool is fully "
|
| 79 |
+
"booked by other users at the moment. Please wait a minute and "
|
| 80 |
+
"retry; demand clears between bursts."
|
| 81 |
+
)
|
| 82 |
+
if "out of memory" in msg or "oom" in msg or "cuda" in msg:
|
| 83 |
+
return (
|
| 84 |
+
"💥 Image generation ran out of GPU memory. Try a smaller "
|
| 85 |
+
"Height/Width or fewer Inference Steps, then retry."
|
| 86 |
+
)
|
| 87 |
+
return (
|
| 88 |
+
"⚠️ Image generation failed. Please try again in a moment — if it "
|
| 89 |
+
"keeps happening, simplify your prompt or lower the resolution."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 94 |
+
def _generate_image_gpu(
|
| 95 |
prompt: str,
|
| 96 |
height: int,
|
| 97 |
width: int,
|
|
|
|
| 99 |
seed: int,
|
| 100 |
randomize_seed: bool,
|
| 101 |
):
|
| 102 |
+
"""The GPU-decorated worker. Runs only under a ZeroGPU allocation; the
|
| 103 |
+
allocator raises *before* this body if no GPU can be granted, which is why
|
| 104 |
+
the rewording lives in the plain `generate_image` wrapper below, not here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
"""
|
| 106 |
if not prompt or not prompt.strip():
|
| 107 |
raise gr.Error("Please enter a prompt.")
|
|
|
|
| 122 |
return _save_image(image), int(seed)
|
| 123 |
|
| 124 |
|
| 125 |
+
def generate_image(
|
| 126 |
+
prompt: str,
|
| 127 |
+
height: int,
|
| 128 |
+
width: int,
|
| 129 |
+
num_inference_steps: int,
|
| 130 |
+
seed: int,
|
| 131 |
+
randomize_seed: bool,
|
| 132 |
+
):
|
| 133 |
+
"""Workflow-facing wrapper around the GPU worker.
|
| 134 |
+
|
| 135 |
+
Bound to the canvas as a `fn` operator node — the workflow calls this
|
| 136 |
+
Python function directly server-side, so the entire pipeline (frontend +
|
| 137 |
+
ZeroGPU) lives in a single Space. This non-GPU wrapper catches rejections
|
| 138 |
+
from the `@spaces.GPU` allocator (which fire before the worker body runs)
|
| 139 |
+
and rewords them into a clear, honest user-facing message.
|
| 140 |
+
|
| 141 |
+
Returns (image_dict, seed_used). The image is serialized to a /gradio_api
|
| 142 |
+
file URL so JSON serialization across the fn bridge succeeds; the
|
| 143 |
+
executor's `fromGradioOutput` turns the dict back into an image port value.
|
| 144 |
+
"""
|
| 145 |
+
try:
|
| 146 |
+
return _generate_image_gpu(
|
| 147 |
+
prompt, height, width, num_inference_steps, seed, randomize_seed
|
| 148 |
+
)
|
| 149 |
+
except gr.Error:
|
| 150 |
+
# Already a user-facing validation message (e.g. empty prompt) — pass
|
| 151 |
+
# it through unchanged.
|
| 152 |
+
raise
|
| 153 |
+
except Exception as e:
|
| 154 |
+
# Allocator rejection (GPU limit / quota / OOM / etc.) — reword.
|
| 155 |
+
raise gr.Error(_friendly_gpu_error(e)) from e
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# The workflow (workflow.json) wires `generate_image` as a `fn` operator:
|
| 159 |
# Prompt, Height, Width, Inference Steps, Seed, Randomize Seed ─▶
|
| 160 |
# generate_image (fn operator, kind="fn") ─▶ Output Image, Seed Used
|
| 161 |
#
|