Spaces:
Sleeping
Implement Hunyuan3D-2.1 generation with background removal
Browse files- generate_hunyuan() is now fully implemented using the vendored
hy3dshape package (tencent/Hunyuan3D-2.1, diffusers-based, no
custom CUDA wheels, works on torch 2.8.0 + ZeroGPU H200)
- @spaces.GPU(duration=240) wraps the local inference call
- Pipeline is loaded lazily on first GPU request and cached globally
- Background removal via rembg (IS-Net) with fallback to hy3dshape's
BackgroundRemover; toggled by do_rembg parameter
- Multi-view support: >1 image routed as {front/left/back/right} dict
- Working mesh is decimated via PyMeshLab to preset face target; full
mesh saved as high_poly.glb for Stage 2 baking
- Added gen_rembg checkbox to the Generate tab (default: on)
- Hunyuan3D-2.1 is now the default model (moved to top of radio)
- Preset save/load includes rembg flag (n=27 components)
- requirements.txt: added diffusers>=0.30.0, accelerate, einops, rembg
- app.py +20 -12
- requirements.txt +12 -6
- src/stages/stage1_generate.py +164 -9
|
@@ -81,11 +81,11 @@ def _stub(stage: str) -> str:
|
|
| 81 |
)
|
| 82 |
|
| 83 |
|
| 84 |
-
def handle_generate(images, model, quality, seed, _steps, _octree, tex_size, _symmetry):
|
| 85 |
"""Dispatch to the correct generation backend based on model selection."""
|
| 86 |
if "TRELLIS" in model:
|
| 87 |
return generate_trellis(images, quality, int(seed), int(tex_size))
|
| 88 |
-
return generate_hunyuan(images, quality, int(seed), int(tex_size))
|
| 89 |
|
| 90 |
|
| 91 |
def run_post_process(
|
|
@@ -284,7 +284,7 @@ def ui_refresh_presets() -> gr.Dropdown:
|
|
| 284 |
|
| 285 |
def ui_save_preset(
|
| 286 |
name: str,
|
| 287 |
-
gen_model, gen_quality, gen_seed, gen_tex_size,
|
| 288 |
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 289 |
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 290 |
pp_albedo, pp_material, pp_ao, pp_ao_quality,
|
|
@@ -296,7 +296,7 @@ def ui_save_preset(
|
|
| 296 |
return "❌ Preset name cannot be empty.", ui_refresh_presets()
|
| 297 |
config = {
|
| 298 |
"name": name, "version": 1, "created_at": time.time(),
|
| 299 |
-
"stage1": {"model": gen_model, "quality": gen_quality, "seed": int(gen_seed), "tex_size": int(gen_tex_size)},
|
| 300 |
"stage2": {
|
| 301 |
"repair": pp_repair, "cleanup": pp_cleanup, "decimate": pp_decimate,
|
| 302 |
"target_faces": int(pp_target_faces), "symmetry": pp_symmetry,
|
|
@@ -315,7 +315,7 @@ def ui_save_preset(
|
|
| 315 |
def ui_load_preset(name: str):
|
| 316 |
"""Apply a saved preset to all UI components."""
|
| 317 |
_no_op = gr.update()
|
| 318 |
-
n =
|
| 319 |
if not name:
|
| 320 |
return ("❌ Select a preset first.",) + (_no_op,) * n
|
| 321 |
try:
|
|
@@ -331,10 +331,11 @@ def ui_load_preset(name: str):
|
|
| 331 |
return (
|
| 332 |
f"✅ Loaded preset: `{name}`",
|
| 333 |
# Stage 1
|
| 334 |
-
gr.update(value=s1.get("model", "
|
| 335 |
gr.update(value=s1.get("quality", "Balanced (~60s)")),
|
| 336 |
gr.update(value=s1.get("seed", 42)),
|
| 337 |
gr.update(value=s1.get("tex_size", 2048)),
|
|
|
|
| 338 |
# Stage 2
|
| 339 |
gr.update(value=s2.get("repair", True)),
|
| 340 |
gr.update(value=s2.get("cleanup", True)),
|
|
@@ -424,10 +425,10 @@ def build_ui() -> gr.Blocks:
|
|
| 424 |
)
|
| 425 |
gen_model = gr.Radio(
|
| 426 |
choices=[
|
|
|
|
| 427 |
"TRELLIS.2 (Hard Surface)",
|
| 428 |
-
"Hunyuan3D-2 (Organic / Characters)",
|
| 429 |
],
|
| 430 |
-
value="
|
| 431 |
label="Generation model",
|
| 432 |
)
|
| 433 |
gen_quality = gr.Radio(
|
|
@@ -435,6 +436,12 @@ def build_ui() -> gr.Blocks:
|
|
| 435 |
value="Balanced (~60s)",
|
| 436 |
label="Quality preset",
|
| 437 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
with gr.Accordion("Advanced", open=False):
|
| 439 |
gen_seed = gr.Number(value=42, label="Seed", precision=0)
|
| 440 |
gen_steps = gr.Slider(
|
|
@@ -442,7 +449,7 @@ def build_ui() -> gr.Blocks:
|
|
| 442 |
label="Inference steps",
|
| 443 |
)
|
| 444 |
gen_octree = gr.Dropdown(
|
| 445 |
-
choices=[
|
| 446 |
label="Octree resolution",
|
| 447 |
)
|
| 448 |
gen_tex_size = gr.Dropdown(
|
|
@@ -460,7 +467,8 @@ def build_ui() -> gr.Blocks:
|
|
| 460 |
gen_btn.click(
|
| 461 |
fn=handle_generate,
|
| 462 |
inputs=[gen_images, gen_model, gen_quality, gen_seed,
|
| 463 |
-
gen_steps, gen_octree, gen_tex_size, gen_symmetry
|
|
|
|
| 464 |
outputs=gen_status,
|
| 465 |
)
|
| 466 |
|
|
@@ -627,7 +635,7 @@ def build_ui() -> gr.Blocks:
|
|
| 627 |
inputs=pr_list,
|
| 628 |
outputs=[
|
| 629 |
pr_status,
|
| 630 |
-
gen_model, gen_quality, gen_seed, gen_tex_size,
|
| 631 |
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 632 |
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 633 |
pp_albedo_bake, pp_material_bake, pp_ao, pp_ao_quality,
|
|
@@ -640,7 +648,7 @@ def build_ui() -> gr.Blocks:
|
|
| 640 |
fn=ui_save_preset,
|
| 641 |
inputs=[
|
| 642 |
pr_name,
|
| 643 |
-
gen_model, gen_quality, gen_seed, gen_tex_size,
|
| 644 |
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 645 |
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 646 |
pp_albedo_bake, pp_material_bake, pp_ao, pp_ao_quality,
|
|
|
|
| 81 |
)
|
| 82 |
|
| 83 |
|
| 84 |
+
def handle_generate(images, model, quality, seed, _steps, _octree, tex_size, _symmetry, do_rembg):
|
| 85 |
"""Dispatch to the correct generation backend based on model selection."""
|
| 86 |
if "TRELLIS" in model:
|
| 87 |
return generate_trellis(images, quality, int(seed), int(tex_size))
|
| 88 |
+
return generate_hunyuan(images, quality, int(seed), int(tex_size), do_rembg=bool(do_rembg))
|
| 89 |
|
| 90 |
|
| 91 |
def run_post_process(
|
|
|
|
| 284 |
|
| 285 |
def ui_save_preset(
|
| 286 |
name: str,
|
| 287 |
+
gen_model, gen_quality, gen_seed, gen_tex_size, gen_rembg,
|
| 288 |
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 289 |
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 290 |
pp_albedo, pp_material, pp_ao, pp_ao_quality,
|
|
|
|
| 296 |
return "❌ Preset name cannot be empty.", ui_refresh_presets()
|
| 297 |
config = {
|
| 298 |
"name": name, "version": 1, "created_at": time.time(),
|
| 299 |
+
"stage1": {"model": gen_model, "quality": gen_quality, "seed": int(gen_seed), "tex_size": int(gen_tex_size), "rembg": bool(gen_rembg)},
|
| 300 |
"stage2": {
|
| 301 |
"repair": pp_repair, "cleanup": pp_cleanup, "decimate": pp_decimate,
|
| 302 |
"target_faces": int(pp_target_faces), "symmetry": pp_symmetry,
|
|
|
|
| 315 |
def ui_load_preset(name: str):
|
| 316 |
"""Apply a saved preset to all UI components."""
|
| 317 |
_no_op = gr.update()
|
| 318 |
+
n = 27 # number of component outputs after pr_status
|
| 319 |
if not name:
|
| 320 |
return ("❌ Select a preset first.",) + (_no_op,) * n
|
| 321 |
try:
|
|
|
|
| 331 |
return (
|
| 332 |
f"✅ Loaded preset: `{name}`",
|
| 333 |
# Stage 1
|
| 334 |
+
gr.update(value=s1.get("model", "Hunyuan3D-2.1 (Organic / Characters)")),
|
| 335 |
gr.update(value=s1.get("quality", "Balanced (~60s)")),
|
| 336 |
gr.update(value=s1.get("seed", 42)),
|
| 337 |
gr.update(value=s1.get("tex_size", 2048)),
|
| 338 |
+
gr.update(value=s1.get("rembg", True)),
|
| 339 |
# Stage 2
|
| 340 |
gr.update(value=s2.get("repair", True)),
|
| 341 |
gr.update(value=s2.get("cleanup", True)),
|
|
|
|
| 425 |
)
|
| 426 |
gen_model = gr.Radio(
|
| 427 |
choices=[
|
| 428 |
+
"Hunyuan3D-2.1 (Organic / Characters)",
|
| 429 |
"TRELLIS.2 (Hard Surface)",
|
|
|
|
| 430 |
],
|
| 431 |
+
value="Hunyuan3D-2.1 (Organic / Characters)",
|
| 432 |
label="Generation model",
|
| 433 |
)
|
| 434 |
gen_quality = gr.Radio(
|
|
|
|
| 436 |
value="Balanced (~60s)",
|
| 437 |
label="Quality preset",
|
| 438 |
)
|
| 439 |
+
gen_rembg = gr.Checkbox(
|
| 440 |
+
value=True,
|
| 441 |
+
label="Remove background automatically",
|
| 442 |
+
info="Uses rembg IS-Net to strip background before generation. "
|
| 443 |
+
"Disable if your image already has a transparent background.",
|
| 444 |
+
)
|
| 445 |
with gr.Accordion("Advanced", open=False):
|
| 446 |
gen_seed = gr.Number(value=42, label="Seed", precision=0)
|
| 447 |
gen_steps = gr.Slider(
|
|
|
|
| 449 |
label="Inference steps",
|
| 450 |
)
|
| 451 |
gen_octree = gr.Dropdown(
|
| 452 |
+
choices=[256, 384, 512], value=384,
|
| 453 |
label="Octree resolution",
|
| 454 |
)
|
| 455 |
gen_tex_size = gr.Dropdown(
|
|
|
|
| 467 |
gen_btn.click(
|
| 468 |
fn=handle_generate,
|
| 469 |
inputs=[gen_images, gen_model, gen_quality, gen_seed,
|
| 470 |
+
gen_steps, gen_octree, gen_tex_size, gen_symmetry,
|
| 471 |
+
gen_rembg],
|
| 472 |
outputs=gen_status,
|
| 473 |
)
|
| 474 |
|
|
|
|
| 635 |
inputs=pr_list,
|
| 636 |
outputs=[
|
| 637 |
pr_status,
|
| 638 |
+
gen_model, gen_quality, gen_seed, gen_tex_size, gen_rembg,
|
| 639 |
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 640 |
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 641 |
pp_albedo_bake, pp_material_bake, pp_ao, pp_ao_quality,
|
|
|
|
| 648 |
fn=ui_save_preset,
|
| 649 |
inputs=[
|
| 650 |
pr_name,
|
| 651 |
+
gen_model, gen_quality, gen_seed, gen_tex_size, gen_rembg,
|
| 652 |
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 653 |
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 654 |
pp_albedo_bake, pp_material_bake, pp_ao, pp_ao_quality,
|
|
@@ -7,12 +7,12 @@ spaces # HF ZeroGPU decorator
|
|
| 7 |
numpy
|
| 8 |
pillow
|
| 9 |
|
| 10 |
-
# ===== Milestone 2: Stage 1 Generation
|
| 11 |
-
#
|
| 12 |
-
#
|
| 13 |
-
#
|
| 14 |
-
#
|
| 15 |
-
gradio_client # call TRELLIS.2
|
| 16 |
torch==2.8.0
|
| 17 |
torchvision==0.23.0
|
| 18 |
imageio==2.37.2
|
|
@@ -26,6 +26,12 @@ zstandard==0.25.0
|
|
| 26 |
kornia==0.8.2
|
| 27 |
timm==1.0.22
|
| 28 |
git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# ===== Milestone 3: Stage 2A-2C Mesh Cleanup =====
|
| 31 |
trimesh[easy]
|
|
|
|
| 7 |
numpy
|
| 8 |
pillow
|
| 9 |
|
| 10 |
+
# ===== Milestone 2: Stage 1 Generation =====
|
| 11 |
+
# TRELLIS.2 generation: via gradio_client (remote Space) — ABI-incompatible custom
|
| 12 |
+
# wheels (o_voxel, cumesh, flex_gemm) cannot run locally on torch 2.8.0+.
|
| 13 |
+
# Hunyuan3D-2.1 generation: local inference via vendored hy3dshape package —
|
| 14 |
+
# diffusers-based, no custom CUDA extensions, works on torch 2.8.0 + ZeroGPU.
|
| 15 |
+
gradio_client # call TRELLIS.2 Space
|
| 16 |
torch==2.8.0
|
| 17 |
torchvision==0.23.0
|
| 18 |
imageio==2.37.2
|
|
|
|
| 26 |
kornia==0.8.2
|
| 27 |
timm==1.0.22
|
| 28 |
git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
|
| 29 |
+
# Hunyuan3D-2.1 dependencies
|
| 30 |
+
diffusers>=0.30.0
|
| 31 |
+
accelerate
|
| 32 |
+
einops
|
| 33 |
+
# Background removal (used before shape generation)
|
| 34 |
+
rembg
|
| 35 |
|
| 36 |
# ===== Milestone 3: Stage 2A-2C Mesh Cleanup =====
|
| 37 |
trimesh[easy]
|
|
@@ -1,25 +1,34 @@
|
|
| 1 |
"""
|
| 2 |
Stage 1 — Image to 3D generation.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
are compiled
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
import shutil
|
|
|
|
| 14 |
import tempfile
|
| 15 |
import time
|
| 16 |
from pathlib import Path
|
| 17 |
|
|
|
|
| 18 |
from PIL import Image
|
| 19 |
|
| 20 |
from src import workspace
|
| 21 |
from src.workspace import CURRENT
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# ---------------------------------------------------------------------------
|
| 24 |
# Quality presets → TRELLIS.2 sampler parameters
|
| 25 |
# ---------------------------------------------------------------------------
|
|
@@ -180,16 +189,162 @@ def generate_trellis(
|
|
| 180 |
|
| 181 |
|
| 182 |
# ---------------------------------------------------------------------------
|
| 183 |
-
# Hunyuan3D-2.1
|
| 184 |
# ---------------------------------------------------------------------------
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
def generate_hunyuan(
|
| 187 |
images: list | None,
|
| 188 |
quality: str,
|
| 189 |
seed: int,
|
| 190 |
tex_size: int = 2048,
|
|
|
|
| 191 |
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
return (
|
| 193 |
-
"
|
| 194 |
-
"
|
|
|
|
|
|
|
|
|
|
| 195 |
)
|
|
|
|
| 1 |
"""
|
| 2 |
Stage 1 — Image to 3D generation.
|
| 3 |
|
| 4 |
+
TRELLIS.2 generation is offloaded via gradio_client to the official HuggingFace
|
| 5 |
+
Space (microsoft/TRELLIS.2) because the TRELLIS.2 custom wheels (o_voxel, cumesh,
|
| 6 |
+
flex_gemm) are compiled for torch 2.6.0 and are ABI-broken on torch 2.8.0+.
|
| 7 |
+
|
| 8 |
+
Hunyuan3D-2.1 runs locally using the vendored hy3dshape package. The shape
|
| 9 |
+
pipeline is diffusers-based with no custom CUDA extensions — it works on torch
|
| 10 |
+
2.8.0 + ZeroGPU H200. The model is streamed from tencent/Hunyuan3D-2.1 on first
|
| 11 |
+
run and cached by HuggingFace Hub.
|
| 12 |
"""
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
import shutil
|
| 16 |
+
import sys
|
| 17 |
import tempfile
|
| 18 |
import time
|
| 19 |
from pathlib import Path
|
| 20 |
|
| 21 |
+
import spaces
|
| 22 |
from PIL import Image
|
| 23 |
|
| 24 |
from src import workspace
|
| 25 |
from src.workspace import CURRENT
|
| 26 |
|
| 27 |
+
# Make the vendored hy3dshape package importable.
|
| 28 |
+
_HY3D_PATH = Path(__file__).resolve().parents[2] / "Hunyuan3D-2.1" / "hy3dshape"
|
| 29 |
+
if str(_HY3D_PATH) not in sys.path:
|
| 30 |
+
sys.path.insert(0, str(_HY3D_PATH))
|
| 31 |
+
|
| 32 |
# ---------------------------------------------------------------------------
|
| 33 |
# Quality presets → TRELLIS.2 sampler parameters
|
| 34 |
# ---------------------------------------------------------------------------
|
|
|
|
| 189 |
|
| 190 |
|
| 191 |
# ---------------------------------------------------------------------------
|
| 192 |
+
# Hunyuan3D-2.1 — local inference using vendored hy3dshape
|
| 193 |
# ---------------------------------------------------------------------------
|
| 194 |
|
| 195 |
+
# Quality presets for Hunyuan3D-2.1
|
| 196 |
+
_HY_PRESETS = {
|
| 197 |
+
"Fast (~30s)": {"steps": 20, "octree_resolution": 256, "decimation_target": 50_000},
|
| 198 |
+
"Balanced (~60s)": {"steps": 35, "octree_resolution": 384, "decimation_target": 100_000},
|
| 199 |
+
"Hero (~90s)": {"steps": 50, "octree_resolution": 512, "decimation_target": 200_000},
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
# Pipeline loaded lazily on first GPU call to avoid startup cost.
|
| 203 |
+
_hy_pipeline = None
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _get_hy_pipeline():
|
| 207 |
+
global _hy_pipeline
|
| 208 |
+
if _hy_pipeline is None:
|
| 209 |
+
from hy3dshape.pipelines import Hunyuan3DDiTFlowMatchingPipeline
|
| 210 |
+
_hy_pipeline = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
|
| 211 |
+
"tencent/Hunyuan3D-2.1",
|
| 212 |
+
subfolder="hunyuan3d-dit-v2-1",
|
| 213 |
+
use_safetensors=False,
|
| 214 |
+
variant="fp16",
|
| 215 |
+
)
|
| 216 |
+
return _hy_pipeline
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _remove_background(image: Image.Image) -> Image.Image:
|
| 220 |
+
"""Strip background using rembg (IS-Net model)."""
|
| 221 |
+
try:
|
| 222 |
+
from rembg import remove, new_session
|
| 223 |
+
session = new_session()
|
| 224 |
+
return remove(image, session=session, bgcolor=[255, 255, 255, 0])
|
| 225 |
+
except Exception as e:
|
| 226 |
+
# Fall back to the hy3dshape wrapper if rembg is unavailable.
|
| 227 |
+
try:
|
| 228 |
+
from hy3dshape.rembg import BackgroundRemover
|
| 229 |
+
remover = BackgroundRemover()
|
| 230 |
+
return remover(image)
|
| 231 |
+
except Exception:
|
| 232 |
+
raise RuntimeError(f"Background removal failed: {e}")
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@spaces.GPU(duration=240)
|
| 236 |
def generate_hunyuan(
|
| 237 |
images: list | None,
|
| 238 |
quality: str,
|
| 239 |
seed: int,
|
| 240 |
tex_size: int = 2048,
|
| 241 |
+
do_rembg: bool = True,
|
| 242 |
) -> str:
|
| 243 |
+
"""Generate a 3D mesh with Hunyuan3D-2.1 and save it to the workspace."""
|
| 244 |
+
import torch
|
| 245 |
+
|
| 246 |
+
t0 = time.time()
|
| 247 |
+
|
| 248 |
+
if not images:
|
| 249 |
+
return "❌ Please upload at least one reference image."
|
| 250 |
+
|
| 251 |
+
# ------------------------------------------------------------------
|
| 252 |
+
# Load and optionally remove background from input image(s)
|
| 253 |
+
# ------------------------------------------------------------------
|
| 254 |
+
def _open(f) -> Image.Image:
|
| 255 |
+
path = f.name if hasattr(f, "name") else str(f)
|
| 256 |
+
return Image.open(path).convert("RGBA")
|
| 257 |
+
|
| 258 |
+
pil_images = [_open(f) for f in images]
|
| 259 |
+
|
| 260 |
+
if do_rembg:
|
| 261 |
+
try:
|
| 262 |
+
pil_images = [_remove_background(img) for img in pil_images]
|
| 263 |
+
except Exception as e:
|
| 264 |
+
return f"❌ Background removal failed:\n```\n{e}\n```"
|
| 265 |
+
|
| 266 |
+
preset = _HY_PRESETS.get(quality, _HY_PRESETS["Balanced (~60s)"])
|
| 267 |
+
|
| 268 |
+
# Multi-view: pass as dict if more than one image is provided.
|
| 269 |
+
if len(pil_images) == 1:
|
| 270 |
+
image_input = pil_images[0]
|
| 271 |
+
else:
|
| 272 |
+
view_keys = ["front", "left", "back", "right"]
|
| 273 |
+
image_input = {view_keys[i]: img for i, img in enumerate(pil_images[:4])}
|
| 274 |
+
|
| 275 |
+
# ------------------------------------------------------------------
|
| 276 |
+
# Shape generation
|
| 277 |
+
# ------------------------------------------------------------------
|
| 278 |
+
try:
|
| 279 |
+
pipeline = _get_hy_pipeline()
|
| 280 |
+
generator = torch.Generator().manual_seed(int(seed))
|
| 281 |
+
meshes = pipeline(
|
| 282 |
+
image=image_input,
|
| 283 |
+
num_inference_steps=preset["steps"],
|
| 284 |
+
octree_resolution=preset["octree_resolution"],
|
| 285 |
+
mc_level=0.0,
|
| 286 |
+
output_type="trimesh",
|
| 287 |
+
generator=generator,
|
| 288 |
+
)
|
| 289 |
+
mesh = meshes[0] if isinstance(meshes, list) else meshes
|
| 290 |
+
except Exception as e:
|
| 291 |
+
return (
|
| 292 |
+
f"❌ Hunyuan3D-2.1 generation failed:\n```\n{type(e).__name__}: {e}\n```\n\n"
|
| 293 |
+
f"Tip: make sure the image has a clean subject on a white/transparent background."
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# ------------------------------------------------------------------
|
| 297 |
+
# Save to workspace
|
| 298 |
+
# ------------------------------------------------------------------
|
| 299 |
+
workspace.reset_current()
|
| 300 |
+
ws = workspace.reset_state()
|
| 301 |
+
|
| 302 |
+
high_poly_path = CURRENT / "high_poly.glb"
|
| 303 |
+
raw_gen_path = CURRENT / "raw_gen.glb"
|
| 304 |
+
mesh.export(str(high_poly_path))
|
| 305 |
+
# Decimate for the working copy
|
| 306 |
+
try:
|
| 307 |
+
import pymeshlab
|
| 308 |
+
import trimesh as _tm
|
| 309 |
+
with tempfile.NamedTemporaryFile(suffix=".ply", delete=False) as tmp:
|
| 310 |
+
ply_in = tmp.name
|
| 311 |
+
mesh.export(ply_in)
|
| 312 |
+
ms = pymeshlab.MeshSet()
|
| 313 |
+
ms.load_new_mesh(ply_in)
|
| 314 |
+
target = max(100, preset["decimation_target"])
|
| 315 |
+
ms.apply_filter(
|
| 316 |
+
"meshing_decimation_quadric_edge_collapse",
|
| 317 |
+
targetfacenum=target,
|
| 318 |
+
preservenormal=True,
|
| 319 |
+
autoclean=True,
|
| 320 |
+
)
|
| 321 |
+
with tempfile.NamedTemporaryFile(suffix=".ply", delete=False) as tmp2:
|
| 322 |
+
ply_out = tmp2.name
|
| 323 |
+
ms.save_current_mesh(ply_out)
|
| 324 |
+
working = _tm.load(ply_out, force="mesh")
|
| 325 |
+
working.export(str(raw_gen_path))
|
| 326 |
+
except Exception:
|
| 327 |
+
# If decimation fails, just copy the full mesh.
|
| 328 |
+
shutil.copy(str(high_poly_path), str(raw_gen_path))
|
| 329 |
+
|
| 330 |
+
face_count = len(mesh.faces)
|
| 331 |
+
vertex_count = len(mesh.vertices)
|
| 332 |
+
|
| 333 |
+
ws.high_poly_glb = high_poly_path
|
| 334 |
+
ws.raw_gen_glb = raw_gen_path
|
| 335 |
+
ws.model_used = "Hunyuan3D-2.1"
|
| 336 |
+
ws.face_count = face_count
|
| 337 |
+
ws.vertex_count = vertex_count
|
| 338 |
+
|
| 339 |
+
elapsed = time.time() - t0
|
| 340 |
+
|
| 341 |
+
from src import quota as _quota
|
| 342 |
+
_quota.record_usage(f"generate_hunyuan_{quality.split()[0].lower()}", elapsed)
|
| 343 |
+
|
| 344 |
return (
|
| 345 |
+
f"✅ **Generated with Hunyuan3D-2.1** · {quality}\n\n"
|
| 346 |
+
f"- Faces: {face_count:,} · Vertices: {vertex_count:,}\n"
|
| 347 |
+
f"- Time: {elapsed:.1f}s\n\n"
|
| 348 |
+
f"Scroll down to see the result in the 3D viewer. "
|
| 349 |
+
f"High-poly saved for Stage 2 baking."
|
| 350 |
)
|