Spaces:
Sleeping
Fix viewer not updating: direct GLB output + allowed_paths
Browse files- Moved gen_btn.click() wiring to after gr.Model3D viewer is defined
so outputs=[gen_status, viewer] is valid (viewer was not in scope
when the click was wired inside Tab 1)
- generate_hunyuan() now yields (status_str, glb_path|None) tuples;
the final yield carries str(raw_gen_path) so Gradio receives the GLB
path as a direct function output and serves it automatically β no
separate state lookup needed
- Added allowed_paths=[workspace.WORKSPACE] to launch() so Gradio 5
can serve files from the workspace directory (without this, local
paths returned to gr.Model3D are silently rejected)
- _gen_event.then() now only refreshes [summary, status_bar] since the
viewer is already updated by the direct output above
- pp/rig/ex still use _global_refresh for all three (viewer, summary,
status_bar) via .then()
- app.py +38 -18
- src/stages/stage1_generate.py +11 -12
|
@@ -82,10 +82,17 @@ def _stub(stage: str) -> str:
|
|
| 82 |
|
| 83 |
|
| 84 |
def handle_generate(images, model, quality, seed, _steps, _octree, tex_size, _symmetry, do_rembg):
|
| 85 |
-
"""Dispatch to the correct generation backend.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
if "TRELLIS" in model:
|
| 87 |
-
yield f"βοΈ **TRELLIS.2** Β· {quality}\n\nContacting remote Space..."
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
return
|
| 90 |
yield from generate_hunyuan(images, quality, int(seed), int(tex_size), do_rembg=bool(do_rembg))
|
| 91 |
|
|
@@ -466,13 +473,7 @@ def build_ui() -> gr.Blocks:
|
|
| 466 |
gen_btn = gr.Button("Generate", variant="primary")
|
| 467 |
with gr.Column(scale=1):
|
| 468 |
gen_status = gr.Markdown("*Awaiting input.*")
|
| 469 |
-
|
| 470 |
-
fn=handle_generate,
|
| 471 |
-
inputs=[gen_images, gen_model, gen_quality, gen_seed,
|
| 472 |
-
gen_steps, gen_octree, gen_tex_size, gen_symmetry,
|
| 473 |
-
gen_rembg],
|
| 474 |
-
outputs=gen_status,
|
| 475 |
-
)
|
| 476 |
|
| 477 |
# ============ Tab 2: Post-Process =============================
|
| 478 |
with gr.Tab("2. Post-Process", id=2):
|
|
@@ -713,10 +714,22 @@ def build_ui() -> gr.Blocks:
|
|
| 713 |
elem_classes=["status-bar"],
|
| 714 |
)
|
| 715 |
|
| 716 |
-
# ---
|
| 717 |
-
#
|
| 718 |
-
#
|
| 719 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
def _global_refresh():
|
| 721 |
return (
|
| 722 |
ui_helpers.get_viewer_model_path(),
|
|
@@ -724,9 +737,14 @@ def build_ui() -> gr.Blocks:
|
|
| 724 |
ui_helpers.get_status_bar(),
|
| 725 |
)
|
| 726 |
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
_ev.then(fn=_global_refresh, outputs=[viewer, summary, status_bar])
|
| 731 |
|
| 732 |
# Utility buttons: refresh immediately on click.
|
|
@@ -749,8 +767,10 @@ demo.queue(default_concurrency_limit=1).launch(
|
|
| 749 |
server_name="0.0.0.0",
|
| 750 |
server_port=7860,
|
| 751 |
show_error=True,
|
|
|
|
|
|
|
|
|
|
| 752 |
# Gradio 6: show_api removed. Use footer_links instead.
|
| 753 |
-
# Equivalent of old show_api=False:
|
| 754 |
footer_links=["gradio", "settings"],
|
| 755 |
theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
|
| 756 |
css=CUSTOM_CSS,
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def handle_generate(images, model, quality, seed, _steps, _octree, tex_size, _symmetry, do_rembg):
|
| 85 |
+
"""Dispatch to the correct generation backend.
|
| 86 |
+
|
| 87 |
+
Yields (status_markdown, viewer_path_or_None) tuples for streaming.
|
| 88 |
+
The viewer path is yielded only in the final tuple so Gradio can serve
|
| 89 |
+
the GLB directly without a separate state lookup.
|
| 90 |
+
"""
|
| 91 |
if "TRELLIS" in model:
|
| 92 |
+
yield f"βοΈ **TRELLIS.2** Β· {quality}\n\nContacting remote Space...", None
|
| 93 |
+
result = generate_trellis(images, quality, int(seed), int(tex_size))
|
| 94 |
+
viewer_path = ui_helpers.get_viewer_model_path()
|
| 95 |
+
yield result, viewer_path
|
| 96 |
return
|
| 97 |
yield from generate_hunyuan(images, quality, int(seed), int(tex_size), do_rembg=bool(do_rembg))
|
| 98 |
|
|
|
|
| 473 |
gen_btn = gr.Button("Generate", variant="primary")
|
| 474 |
with gr.Column(scale=1):
|
| 475 |
gen_status = gr.Markdown("*Awaiting input.*")
|
| 476 |
+
# Click wired below, after viewer component is defined.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
# ============ Tab 2: Post-Process =============================
|
| 479 |
with gr.Tab("2. Post-Process", id=2):
|
|
|
|
| 714 |
elem_classes=["status-bar"],
|
| 715 |
)
|
| 716 |
|
| 717 |
+
# --- Wire generate button now that viewer is in scope ---------------
|
| 718 |
+
# handle_generate yields (status_str, glb_path_or_None) tuples so that
|
| 719 |
+
# the viewer updates the instant the final mesh is ready β no separate
|
| 720 |
+
# state lookup needed.
|
| 721 |
+
_gen_event = gen_btn.click(
|
| 722 |
+
fn=handle_generate,
|
| 723 |
+
inputs=[gen_images, gen_model, gen_quality, gen_seed,
|
| 724 |
+
gen_steps, gen_octree, gen_tex_size, gen_symmetry,
|
| 725 |
+
gen_rembg],
|
| 726 |
+
outputs=[gen_status, viewer],
|
| 727 |
+
)
|
| 728 |
+
|
| 729 |
+
# --- Global refresh: every pipeline action updates summary + status bar.
|
| 730 |
+
# For gen_btn the viewer is already updated directly above; for the rest
|
| 731 |
+
# we include the viewer in _global_refresh so post-process / rig / export
|
| 732 |
+
# results also appear.
|
| 733 |
def _global_refresh():
|
| 734 |
return (
|
| 735 |
ui_helpers.get_viewer_model_path(),
|
|
|
|
| 737 |
ui_helpers.get_status_bar(),
|
| 738 |
)
|
| 739 |
|
| 740 |
+
def _summary_refresh():
|
| 741 |
+
return ui_helpers.get_asset_summary(), ui_helpers.get_status_bar()
|
| 742 |
+
|
| 743 |
+
# gen_btn: viewer already updated by direct output; only refresh metadata.
|
| 744 |
+
_gen_event.then(fn=_summary_refresh, outputs=[summary, status_bar])
|
| 745 |
+
|
| 746 |
+
# Other processing buttons: refresh viewer + metadata after completion.
|
| 747 |
+
for _ev in (_pp_event, _rig_event, _ex_event):
|
| 748 |
_ev.then(fn=_global_refresh, outputs=[viewer, summary, status_bar])
|
| 749 |
|
| 750 |
# Utility buttons: refresh immediately on click.
|
|
|
|
| 767 |
server_name="0.0.0.0",
|
| 768 |
server_port=7860,
|
| 769 |
show_error=True,
|
| 770 |
+
# Allow Gradio to serve files from the workspace directory so that
|
| 771 |
+
# gr.Model3D can display generated GLBs without a 403 error.
|
| 772 |
+
allowed_paths=[str(workspace.WORKSPACE)],
|
| 773 |
# Gradio 6: show_api removed. Use footer_links instead.
|
|
|
|
| 774 |
footer_links=["gradio", "settings"],
|
| 775 |
theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
|
| 776 |
css=CUSTOM_CSS,
|
|
@@ -247,13 +247,13 @@ def generate_hunyuan(
|
|
| 247 |
hdr = f"βοΈ **Hunyuan3D-2.1** Β· {quality}\n\n"
|
| 248 |
|
| 249 |
if not images:
|
| 250 |
-
yield "β Please upload at least one reference image."
|
| 251 |
return
|
| 252 |
|
| 253 |
# ------------------------------------------------------------------
|
| 254 |
# Step 1 β Load images
|
| 255 |
# ------------------------------------------------------------------
|
| 256 |
-
yield hdr + "**[1/5]** Loading images..."
|
| 257 |
|
| 258 |
def _open(f) -> Image.Image:
|
| 259 |
path = f.name if hasattr(f, "name") else str(f)
|
|
@@ -265,14 +265,14 @@ def generate_hunyuan(
|
|
| 265 |
# Step 2 β Background removal
|
| 266 |
# ------------------------------------------------------------------
|
| 267 |
if do_rembg:
|
| 268 |
-
yield hdr + f"**[2/5]** Removing background ({len(pil_images)} image(s))..."
|
| 269 |
try:
|
| 270 |
pil_images = [_remove_background(img) for img in pil_images]
|
| 271 |
except Exception as e:
|
| 272 |
-
yield f"β Background removal failed:\n```\n{e}\n```"
|
| 273 |
return
|
| 274 |
else:
|
| 275 |
-
yield hdr + "**[2/5]** Background removal skipped."
|
| 276 |
|
| 277 |
preset = _HY_PRESETS.get(quality, _HY_PRESETS["Balanced (~60s)"])
|
| 278 |
|
|
@@ -285,12 +285,12 @@ def generate_hunyuan(
|
|
| 285 |
# ------------------------------------------------------------------
|
| 286 |
# Step 3 β Load pipeline
|
| 287 |
# ------------------------------------------------------------------
|
| 288 |
-
yield hdr + "**[3/5]** Loading pipeline (first run downloads ~
|
| 289 |
|
| 290 |
try:
|
| 291 |
pipeline = _get_hy_pipeline()
|
| 292 |
except Exception as e:
|
| 293 |
-
yield f"β Pipeline load failed:\n```\n{type(e).__name__}: {e}\n```"
|
| 294 |
return
|
| 295 |
|
| 296 |
# ------------------------------------------------------------------
|
|
@@ -298,7 +298,7 @@ def generate_hunyuan(
|
|
| 298 |
# ------------------------------------------------------------------
|
| 299 |
steps = preset["steps"]
|
| 300 |
octree = preset["octree_resolution"]
|
| 301 |
-
yield hdr + f"**[4/5]** Generating shape β {steps} diffusion steps, octree {octree}..."
|
| 302 |
|
| 303 |
# Track step count via callback so we can update progress.
|
| 304 |
_progress: list[int] = [0]
|
|
@@ -323,13 +323,13 @@ def generate_hunyuan(
|
|
| 323 |
yield (
|
| 324 |
f"β Hunyuan3D-2.1 generation failed:\n```\n{type(e).__name__}: {e}\n```\n\n"
|
| 325 |
f"Tip: make sure the image has a clean subject on a white/transparent background."
|
| 326 |
-
)
|
| 327 |
return
|
| 328 |
|
| 329 |
# ------------------------------------------------------------------
|
| 330 |
# Step 5 β Save & decimate
|
| 331 |
# ------------------------------------------------------------------
|
| 332 |
-
yield hdr + "**[5/5]** Saving mesh and building working copy..."
|
| 333 |
|
| 334 |
workspace.reset_current()
|
| 335 |
ws = workspace.reset_state()
|
|
@@ -378,6 +378,5 @@ def generate_hunyuan(
|
|
| 378 |
f"β
**Generated with Hunyuan3D-2.1** Β· {quality}\n\n"
|
| 379 |
f"- Faces: {face_count:,} Β· Vertices: {vertex_count:,}\n"
|
| 380 |
f"- Time: {elapsed:.1f}s\n\n"
|
| 381 |
-
f"Scroll down to see the result in the 3D viewer. "
|
| 382 |
f"High-poly saved for Stage 2 baking."
|
| 383 |
-
)
|
|
|
|
| 247 |
hdr = f"βοΈ **Hunyuan3D-2.1** Β· {quality}\n\n"
|
| 248 |
|
| 249 |
if not images:
|
| 250 |
+
yield "β Please upload at least one reference image.", None
|
| 251 |
return
|
| 252 |
|
| 253 |
# ------------------------------------------------------------------
|
| 254 |
# Step 1 β Load images
|
| 255 |
# ------------------------------------------------------------------
|
| 256 |
+
yield hdr + "**[1/5]** Loading images...", None
|
| 257 |
|
| 258 |
def _open(f) -> Image.Image:
|
| 259 |
path = f.name if hasattr(f, "name") else str(f)
|
|
|
|
| 265 |
# Step 2 β Background removal
|
| 266 |
# ------------------------------------------------------------------
|
| 267 |
if do_rembg:
|
| 268 |
+
yield hdr + f"**[2/5]** Removing background ({len(pil_images)} image(s))...", None
|
| 269 |
try:
|
| 270 |
pil_images = [_remove_background(img) for img in pil_images]
|
| 271 |
except Exception as e:
|
| 272 |
+
yield f"β Background removal failed:\n```\n{e}\n```", None
|
| 273 |
return
|
| 274 |
else:
|
| 275 |
+
yield hdr + "**[2/5]** Background removal skipped.", None
|
| 276 |
|
| 277 |
preset = _HY_PRESETS.get(quality, _HY_PRESETS["Balanced (~60s)"])
|
| 278 |
|
|
|
|
| 285 |
# ------------------------------------------------------------------
|
| 286 |
# Step 3 β Load pipeline
|
| 287 |
# ------------------------------------------------------------------
|
| 288 |
+
yield hdr + "**[3/5]** Loading pipeline (first run downloads ~7 GB of weights)...", None
|
| 289 |
|
| 290 |
try:
|
| 291 |
pipeline = _get_hy_pipeline()
|
| 292 |
except Exception as e:
|
| 293 |
+
yield f"β Pipeline load failed:\n```\n{type(e).__name__}: {e}\n```", None
|
| 294 |
return
|
| 295 |
|
| 296 |
# ------------------------------------------------------------------
|
|
|
|
| 298 |
# ------------------------------------------------------------------
|
| 299 |
steps = preset["steps"]
|
| 300 |
octree = preset["octree_resolution"]
|
| 301 |
+
yield hdr + f"**[4/5]** Generating shape β {steps} diffusion steps, octree {octree}...", None
|
| 302 |
|
| 303 |
# Track step count via callback so we can update progress.
|
| 304 |
_progress: list[int] = [0]
|
|
|
|
| 323 |
yield (
|
| 324 |
f"β Hunyuan3D-2.1 generation failed:\n```\n{type(e).__name__}: {e}\n```\n\n"
|
| 325 |
f"Tip: make sure the image has a clean subject on a white/transparent background."
|
| 326 |
+
), None
|
| 327 |
return
|
| 328 |
|
| 329 |
# ------------------------------------------------------------------
|
| 330 |
# Step 5 β Save & decimate
|
| 331 |
# ------------------------------------------------------------------
|
| 332 |
+
yield hdr + "**[5/5]** Saving mesh and building working copy...", None
|
| 333 |
|
| 334 |
workspace.reset_current()
|
| 335 |
ws = workspace.reset_state()
|
|
|
|
| 378 |
f"β
**Generated with Hunyuan3D-2.1** Β· {quality}\n\n"
|
| 379 |
f"- Faces: {face_count:,} Β· Vertices: {vertex_count:,}\n"
|
| 380 |
f"- Time: {elapsed:.1f}s\n\n"
|
|
|
|
| 381 |
f"High-poly saved for Stage 2 baking."
|
| 382 |
+
), str(raw_gen_path)
|