Spaces:
Running on Zero
Running on Zero
UI: min-height 500px auto-sizing output, gr.Progress step reporting
Browse files
app.py
CHANGED
|
@@ -11,6 +11,8 @@ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
|
| 11 |
import spaces # MUST be imported before torch / transformers / sensenova_u1
|
| 12 |
|
| 13 |
import random
|
|
|
|
|
|
|
| 14 |
|
| 15 |
import gradio as gr
|
| 16 |
import numpy as np
|
|
@@ -211,6 +213,27 @@ def _estimate_duration(pil_inputs, prompt, aspect_ratio, seed, *args, **kwargs):
|
|
| 211 |
return 120
|
| 212 |
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
@spaces.GPU(duration=_estimate_duration)
|
| 215 |
def _generate_on_gpu(
|
| 216 |
pil_inputs: list,
|
|
@@ -218,52 +241,82 @@ def _generate_on_gpu(
|
|
| 218 |
aspect_ratio: str,
|
| 219 |
seed: int,
|
| 220 |
):
|
| 221 |
-
"""GPU worker. Inputs are already validated / preprocessed on CPU.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
torch.cuda.reset_peak_memory_stats()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
try:
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
cfg_scale=DEFAULT_CFG_SCALE,
|
| 232 |
-
cfg_norm="none",
|
| 233 |
-
timestep_shift=DEFAULT_TIMESTEP_SHIFT,
|
| 234 |
-
cfg_interval=(0.0, 1.0),
|
| 235 |
-
num_steps=DEFAULT_NUM_STEPS,
|
| 236 |
-
batch_size=1,
|
| 237 |
-
seed=int(seed),
|
| 238 |
-
think_mode=False,
|
| 239 |
-
)
|
| 240 |
-
else:
|
| 241 |
-
out_w, out_h = _editing_output_size(pil_inputs[0], EDIT_TARGET_PIXELS)
|
| 242 |
-
tensor = model.it2i_generate(
|
| 243 |
-
tokenizer,
|
| 244 |
-
prompt,
|
| 245 |
-
pil_inputs,
|
| 246 |
-
image_size=(out_w, out_h),
|
| 247 |
-
cfg_scale=DEFAULT_CFG_SCALE,
|
| 248 |
-
img_cfg_scale=1.0,
|
| 249 |
-
cfg_norm="none",
|
| 250 |
-
timestep_shift=DEFAULT_TIMESTEP_SHIFT,
|
| 251 |
-
cfg_interval=(0.0, 1.0),
|
| 252 |
-
num_steps=DEFAULT_NUM_STEPS,
|
| 253 |
-
batch_size=1,
|
| 254 |
-
think_mode=False,
|
| 255 |
-
seed=int(seed),
|
| 256 |
-
)
|
| 257 |
-
images_out = _to_pil(tensor)
|
| 258 |
-
del tensor
|
| 259 |
print(f"[generate] peak GPU memory: {torch.cuda.max_memory_allocated() / 2**30:.1f} GiB")
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
| 266 |
finally:
|
|
|
|
|
|
|
| 267 |
# Never leave a failed call's tensors behind for the next request.
|
| 268 |
torch.cuda.empty_cache()
|
| 269 |
|
|
@@ -274,7 +327,7 @@ def generate(
|
|
| 274 |
aspect_ratio: str = "1:1",
|
| 275 |
seed: int = 42,
|
| 276 |
randomize_seed: bool = True,
|
| 277 |
-
progress=gr.Progress(
|
| 278 |
):
|
| 279 |
"""Generate an image from a text prompt, or edit an uploaded image.
|
| 280 |
|
|
@@ -307,7 +360,31 @@ def generate(
|
|
| 307 |
sizes = ", ".join(f"{im.width}x{im.height}" for im in pil_inputs)
|
| 308 |
print(f"[generate] editing with {len(pil_inputs)} input image(s) at {sizes}")
|
| 309 |
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
return image_out, seed
|
| 312 |
|
| 313 |
|
|
@@ -316,6 +393,7 @@ def generate_t2i(
|
|
| 316 |
aspect_ratio: str = "1:1",
|
| 317 |
seed: int = 42,
|
| 318 |
randomize_seed: bool = True,
|
|
|
|
| 319 |
):
|
| 320 |
"""Text-to-image only: generate an image from a prompt (no input image).
|
| 321 |
|
|
@@ -328,7 +406,7 @@ def generate_t2i(
|
|
| 328 |
# Wired to the text-to-image gr.Examples: those rows carry (prompt,
|
| 329 |
# aspect_ratio) only, so they must NOT be bound straight to `generate`,
|
| 330 |
# whose first argument is the input-image list.
|
| 331 |
-
return generate(None, prompt, aspect_ratio, seed, randomize_seed)
|
| 332 |
|
| 333 |
|
| 334 |
def generate_edit(
|
|
@@ -336,6 +414,7 @@ def generate_edit(
|
|
| 336 |
prompt: str,
|
| 337 |
seed: int = 42,
|
| 338 |
randomize_seed: bool = True,
|
|
|
|
| 339 |
):
|
| 340 |
"""Image editing: apply an edit instruction to the uploaded image(s).
|
| 341 |
|
|
@@ -345,7 +424,7 @@ def generate_edit(
|
|
| 345 |
seed: RNG seed for reproducible sampling.
|
| 346 |
randomize_seed: if True, pick a fresh random seed each run.
|
| 347 |
"""
|
| 348 |
-
return generate(images, prompt, "1:1", seed, randomize_seed)
|
| 349 |
|
| 350 |
|
| 351 |
# T2I examples: prompt + aspect ratio
|
|
@@ -384,6 +463,18 @@ EDIT_EXAMPLES = [
|
|
| 384 |
CSS = """
|
| 385 |
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 386 |
.dark .gradio-container { color: var(--body-text-color); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
"""
|
| 388 |
|
| 389 |
with gr.Blocks(title="SenseNova-U1.5-8B-MoT") as demo:
|
|
@@ -423,7 +514,15 @@ with gr.Blocks(title="SenseNova-U1.5-8B-MoT") as demo:
|
|
| 423 |
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
| 424 |
generate_button = gr.Button("Generate", variant="primary")
|
| 425 |
with gr.Column(scale=1):
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
used_seed = gr.Number(label="Seed used", interactive=False)
|
| 428 |
|
| 429 |
with gr.Accordion("Text-to-Image Examples", open=True):
|
|
|
|
| 11 |
import spaces # MUST be imported before torch / transformers / sensenova_u1
|
| 12 |
|
| 13 |
import random
|
| 14 |
+
import threading
|
| 15 |
+
import time
|
| 16 |
|
| 17 |
import gradio as gr
|
| 18 |
import numpy as np
|
|
|
|
| 213 |
return 120
|
| 214 |
|
| 215 |
|
| 216 |
+
def _register_step_hook(counter: dict) -> object | None:
|
| 217 |
+
"""Count denoising steps by hooking the per-step timestep embedder.
|
| 218 |
+
|
| 219 |
+
The sampling loops in ``t2i_generate`` / ``it2i_generate`` call
|
| 220 |
+
``fm_modules['timestep_embedder']`` exactly once per step, so a forward
|
| 221 |
+
hook on it is a cheap, non-invasive progress signal.
|
| 222 |
+
"""
|
| 223 |
+
try:
|
| 224 |
+
embedder = model.fm_modules["timestep_embedder"]
|
| 225 |
+
except Exception: # pragma: no cover - defensive
|
| 226 |
+
return None
|
| 227 |
+
|
| 228 |
+
def _tick(_module, _args, _output):
|
| 229 |
+
counter["step"] += 1
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
return embedder.register_forward_hook(_tick)
|
| 233 |
+
except Exception: # pragma: no cover - defensive
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
|
| 237 |
@spaces.GPU(duration=_estimate_duration)
|
| 238 |
def _generate_on_gpu(
|
| 239 |
pil_inputs: list,
|
|
|
|
| 241 |
aspect_ratio: str,
|
| 242 |
seed: int,
|
| 243 |
):
|
| 244 |
+
"""GPU worker. Inputs are already validated / preprocessed on CPU.
|
| 245 |
+
|
| 246 |
+
Runs sampling on a background thread inside the GPU fork and yields
|
| 247 |
+
``("progress", steps_done, total_steps)`` ticks while it works, then a
|
| 248 |
+
final ``("result", image)``. The caller turns those ticks into a
|
| 249 |
+
``gr.Progress`` bar on the output component.
|
| 250 |
+
"""
|
| 251 |
torch.cuda.reset_peak_memory_stats()
|
| 252 |
+
counter = {"step": 0}
|
| 253 |
+
total_steps = DEFAULT_NUM_STEPS
|
| 254 |
+
hook = _register_step_hook(counter)
|
| 255 |
+
result: dict = {}
|
| 256 |
+
|
| 257 |
+
def _run():
|
| 258 |
+
try:
|
| 259 |
+
with torch.inference_mode():
|
| 260 |
+
if not pil_inputs:
|
| 261 |
+
width, height = T2I_RESOLUTIONS[aspect_ratio]
|
| 262 |
+
tensor = model.t2i_generate(
|
| 263 |
+
tokenizer,
|
| 264 |
+
prompt,
|
| 265 |
+
image_size=(width, height),
|
| 266 |
+
cfg_scale=DEFAULT_CFG_SCALE,
|
| 267 |
+
cfg_norm="none",
|
| 268 |
+
timestep_shift=DEFAULT_TIMESTEP_SHIFT,
|
| 269 |
+
cfg_interval=(0.0, 1.0),
|
| 270 |
+
num_steps=DEFAULT_NUM_STEPS,
|
| 271 |
+
batch_size=1,
|
| 272 |
+
seed=int(seed),
|
| 273 |
+
think_mode=False,
|
| 274 |
+
)
|
| 275 |
+
else:
|
| 276 |
+
out_w, out_h = _editing_output_size(pil_inputs[0], EDIT_TARGET_PIXELS)
|
| 277 |
+
tensor = model.it2i_generate(
|
| 278 |
+
tokenizer,
|
| 279 |
+
prompt,
|
| 280 |
+
pil_inputs,
|
| 281 |
+
image_size=(out_w, out_h),
|
| 282 |
+
cfg_scale=DEFAULT_CFG_SCALE,
|
| 283 |
+
img_cfg_scale=1.0,
|
| 284 |
+
cfg_norm="none",
|
| 285 |
+
timestep_shift=DEFAULT_TIMESTEP_SHIFT,
|
| 286 |
+
cfg_interval=(0.0, 1.0),
|
| 287 |
+
num_steps=DEFAULT_NUM_STEPS,
|
| 288 |
+
batch_size=1,
|
| 289 |
+
think_mode=False,
|
| 290 |
+
seed=int(seed),
|
| 291 |
+
)
|
| 292 |
+
result["image"] = _to_pil(tensor)[0]
|
| 293 |
+
del tensor
|
| 294 |
+
except BaseException as exc: # re-raised on the generator thread below
|
| 295 |
+
result["error"] = exc
|
| 296 |
+
|
| 297 |
+
worker = threading.Thread(target=_run, daemon=True)
|
| 298 |
+
started = time.time()
|
| 299 |
try:
|
| 300 |
+
worker.start()
|
| 301 |
+
while True:
|
| 302 |
+
worker.join(0.4)
|
| 303 |
+
done = min(counter["step"], total_steps)
|
| 304 |
+
if not worker.is_alive():
|
| 305 |
+
break
|
| 306 |
+
yield ("progress", done, total_steps, time.time() - started)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
print(f"[generate] peak GPU memory: {torch.cuda.max_memory_allocated() / 2**30:.1f} GiB")
|
| 308 |
+
exc = result.get("error")
|
| 309 |
+
if isinstance(exc, torch.cuda.OutOfMemoryError): # pragma: no cover
|
| 310 |
+
raise gr.Error(
|
| 311 |
+
"Ran out of GPU memory for this request. Try a smaller input image "
|
| 312 |
+
"or a smaller aspect ratio."
|
| 313 |
+
) from exc
|
| 314 |
+
if exc is not None:
|
| 315 |
+
raise exc
|
| 316 |
+
yield ("result", result["image"], total_steps, time.time() - started)
|
| 317 |
finally:
|
| 318 |
+
if hook is not None:
|
| 319 |
+
hook.remove()
|
| 320 |
# Never leave a failed call's tensors behind for the next request.
|
| 321 |
torch.cuda.empty_cache()
|
| 322 |
|
|
|
|
| 327 |
aspect_ratio: str = "1:1",
|
| 328 |
seed: int = 42,
|
| 329 |
randomize_seed: bool = True,
|
| 330 |
+
progress=gr.Progress(),
|
| 331 |
):
|
| 332 |
"""Generate an image from a text prompt, or edit an uploaded image.
|
| 333 |
|
|
|
|
| 360 |
sizes = ", ".join(f"{im.width}x{im.height}" for im in pil_inputs)
|
| 361 |
print(f"[generate] editing with {len(pil_inputs)} input image(s) at {sizes}")
|
| 362 |
|
| 363 |
+
# Drive a progress bar on the output component from the GPU worker's ticks.
|
| 364 |
+
estimated = _estimate_duration(pil_inputs, prompt, aspect_ratio, seed)
|
| 365 |
+
progress(0.0, desc="Starting generation…")
|
| 366 |
+
image_out = None
|
| 367 |
+
for kind, payload, total_steps, elapsed in _generate_on_gpu(
|
| 368 |
+
pil_inputs, prompt, aspect_ratio, seed
|
| 369 |
+
):
|
| 370 |
+
if kind == "progress":
|
| 371 |
+
steps_done = int(payload)
|
| 372 |
+
if steps_done > 0:
|
| 373 |
+
progress(
|
| 374 |
+
min(steps_done / max(total_steps, 1), 1.0),
|
| 375 |
+
desc=f"Denoising step {steps_done}/{total_steps}",
|
| 376 |
+
)
|
| 377 |
+
else:
|
| 378 |
+
# Prefix / conditioning pass, before the first denoising step.
|
| 379 |
+
progress(
|
| 380 |
+
min(elapsed / max(estimated, 1), 0.05),
|
| 381 |
+
desc="Encoding prompt…",
|
| 382 |
+
)
|
| 383 |
+
else:
|
| 384 |
+
image_out = payload
|
| 385 |
+
progress(1.0, desc="Done")
|
| 386 |
+
if image_out is None: # pragma: no cover - worker always yields a result
|
| 387 |
+
raise gr.Error("Generation produced no image. Please try again.")
|
| 388 |
return image_out, seed
|
| 389 |
|
| 390 |
|
|
|
|
| 393 |
aspect_ratio: str = "1:1",
|
| 394 |
seed: int = 42,
|
| 395 |
randomize_seed: bool = True,
|
| 396 |
+
progress=gr.Progress(),
|
| 397 |
):
|
| 398 |
"""Text-to-image only: generate an image from a prompt (no input image).
|
| 399 |
|
|
|
|
| 406 |
# Wired to the text-to-image gr.Examples: those rows carry (prompt,
|
| 407 |
# aspect_ratio) only, so they must NOT be bound straight to `generate`,
|
| 408 |
# whose first argument is the input-image list.
|
| 409 |
+
return generate(None, prompt, aspect_ratio, seed, randomize_seed, progress=progress)
|
| 410 |
|
| 411 |
|
| 412 |
def generate_edit(
|
|
|
|
| 414 |
prompt: str,
|
| 415 |
seed: int = 42,
|
| 416 |
randomize_seed: bool = True,
|
| 417 |
+
progress=gr.Progress(),
|
| 418 |
):
|
| 419 |
"""Image editing: apply an edit instruction to the uploaded image(s).
|
| 420 |
|
|
|
|
| 424 |
seed: RNG seed for reproducible sampling.
|
| 425 |
randomize_seed: if True, pick a fresh random seed each run.
|
| 426 |
"""
|
| 427 |
+
return generate(images, prompt, "1:1", seed, randomize_seed, progress=progress)
|
| 428 |
|
| 429 |
|
| 430 |
# T2I examples: prompt + aspect ratio
|
|
|
|
| 463 |
CSS = """
|
| 464 |
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 465 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 466 |
+
|
| 467 |
+
/* Output: let the frame grow to the natural height of the displayed image,
|
| 468 |
+
with a 500px floor so an empty / small result never collapses. */
|
| 469 |
+
#output-image { min-height: 500px; }
|
| 470 |
+
#output-image .image-container,
|
| 471 |
+
#output-image .image-frame,
|
| 472 |
+
#output-image .empty { min-height: 500px; height: auto; }
|
| 473 |
+
#output-image img {
|
| 474 |
+
height: auto;
|
| 475 |
+
max-height: none;
|
| 476 |
+
object-fit: contain;
|
| 477 |
+
}
|
| 478 |
"""
|
| 479 |
|
| 480 |
with gr.Blocks(title="SenseNova-U1.5-8B-MoT") as demo:
|
|
|
|
| 514 |
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
| 515 |
generate_button = gr.Button("Generate", variant="primary")
|
| 516 |
with gr.Column(scale=1):
|
| 517 |
+
# The output component sizes itself to the generated image (no fixed
|
| 518 |
+
# `height`), with a 500px minimum enforced in CSS via this elem_id.
|
| 519 |
+
output_image = gr.Image(
|
| 520 |
+
label="Output",
|
| 521 |
+
type="pil",
|
| 522 |
+
format="png",
|
| 523 |
+
interactive=False,
|
| 524 |
+
elem_id="output-image",
|
| 525 |
+
)
|
| 526 |
used_seed = gr.Number(label="Seed used", interactive=False)
|
| 527 |
|
| 528 |
with gr.Accordion("Text-to-Image Examples", open=True):
|