multimodalart HF Staff commited on
Commit
e790b7b
·
verified ·
1 Parent(s): 90c3ba1

UI: min-height 500px auto-sizing output, gr.Progress step reporting

Browse files
Files changed (1) hide show
  1. app.py +146 -47
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
- with torch.inference_mode():
225
- if not pil_inputs:
226
- width, height = T2I_RESOLUTIONS[aspect_ratio]
227
- tensor = model.t2i_generate(
228
- tokenizer,
229
- prompt,
230
- image_size=(width, height),
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
- return images_out[0]
261
- except torch.cuda.OutOfMemoryError as exc: # pragma: no cover - depends on load
262
- raise gr.Error(
263
- "Ran out of GPU memory for this request. Try a smaller input image "
264
- "or a smaller aspect ratio."
265
- ) from exc
 
 
 
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(track_tqdm=True),
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
- image_out = _generate_on_gpu(pil_inputs, prompt, aspect_ratio, seed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- output_image = gr.Image(label="Output", type="pil", format="png", interactive=False)
 
 
 
 
 
 
 
 
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):