Spaces:
Sleeping
Sleeping
| """ | |
| Hunyuan3D-2 — Shape-only HuggingFace Space | |
| Uses Hunyuan3D-2mini-Turbo (0.6 B, step-distilled) for fast shape generation | |
| within standard ZeroGPU quota. No texture pipeline — mesh only. | |
| Background removal is skipped — designed for clean character art. | |
| """ | |
| import os | |
| import tempfile | |
| import gradio as gr | |
| import spaces # ZeroGPU decorator | |
| import torch | |
| from PIL import Image | |
| # --------------------------------------------------------------------------- | |
| # Lazy global pipeline — loaded once on first GPU call | |
| # --------------------------------------------------------------------------- | |
| _pipeline = None | |
| def _get_pipeline(): | |
| """Load the shape pipeline once and cache it globally.""" | |
| global _pipeline | |
| if _pipeline is None: | |
| from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline | |
| _pipeline = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained( | |
| "tencent/Hunyuan3D-2mini", | |
| subfolder="hunyuan3d-dit-v2-mini-turbo", | |
| use_safetensors=True, | |
| torch_dtype=torch.float16, | |
| ) | |
| return _pipeline | |
| # --------------------------------------------------------------------------- | |
| # Simple image pre-processing — resize only, no background removal | |
| # --------------------------------------------------------------------------- | |
| def preprocess_image(pil_image: Image.Image) -> Image.Image: | |
| """Resize to 512x512 RGB — the model's native conditioning resolution.""" | |
| return pil_image.convert("RGB").resize((512, 512), Image.LANCZOS) | |
| # --------------------------------------------------------------------------- | |
| # Core generation — wrapped in @spaces.GPU for ZeroGPU | |
| # --------------------------------------------------------------------------- | |
| def generate_shape(image: Image.Image, seed: int, steps: int, octree_res: int): | |
| """ | |
| Run Hunyuan3D-DiT shape generation and return a GLB file path. | |
| NOTE: pipeline.to("cuda") must NOT be reassigned — some custom pipelines | |
| return None from .to(), which would make the pipeline uncallable. | |
| """ | |
| pipeline = _get_pipeline() | |
| pipeline.to("cuda") # move in-place; do not reassign the return value | |
| generator = torch.Generator(device="cuda").manual_seed(seed) | |
| meshes = pipeline( | |
| image=image, | |
| num_inference_steps=steps, | |
| octree_resolution=octree_res, | |
| num_chunks=8000, | |
| generator=generator, | |
| output_type="trimesh", | |
| ) | |
| mesh = meshes[0] | |
| tmp_dir = tempfile.mkdtemp() | |
| out_path = os.path.join(tmp_dir, "shape.glb") | |
| mesh.export(out_path) | |
| return out_path | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| def run(image, seed, steps, octree_res, progress=gr.Progress(track_tqdm=True)): | |
| if image is None: | |
| raise gr.Error("Please upload an image first.") | |
| progress(0.1, desc="Preprocessing image ...") | |
| pil = image if isinstance(image, Image.Image) else Image.fromarray(image) | |
| processed = preprocess_image(pil) | |
| progress(0.3, desc="Running shape diffusion ...") | |
| glb_path = generate_shape(processed, int(seed), int(steps), int(octree_res)) | |
| progress(1.0, desc="Done!") | |
| return glb_path, processed, glb_path | |
| with gr.Blocks(title="Hunyuan3D-2 Shape Generator", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| """ | |
| # Hunyuan3D-2 Shape Generator | |
| Upload character art to generate an **untextured 3-D mesh** using | |
| [Hunyuan3D-2mini-Turbo](https://huggingface.co/tencent/Hunyuan3D-2mini). | |
| Shape only - no texture - stays well within the ZeroGPU free quota. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| input_image = gr.Image( | |
| label="Input Image", | |
| type="pil", | |
| sources=["upload", "clipboard"], | |
| height=340, | |
| ) | |
| with gr.Accordion("Advanced settings", open=False): | |
| seed = gr.Slider( | |
| label="Seed", | |
| minimum=0, maximum=2**31 - 1, | |
| value=42, step=1, | |
| ) | |
| steps = gr.Slider( | |
| label="Diffusion steps", | |
| minimum=5, maximum=50, | |
| value=5, step=1, | |
| info="5-15 works well with the turbo model.", | |
| ) | |
| octree_res = gr.Slider( | |
| label="Octree resolution", | |
| minimum=128, maximum=512, | |
| value=192, step=64, | |
| info="Higher = finer mesh detail but more VRAM & time.", | |
| ) | |
| generate_btn = gr.Button("Generate Shape", variant="primary") | |
| with gr.Column(scale=1): | |
| preview_img = gr.Image( | |
| label="Image sent to model (512x512)", | |
| type="pil", | |
| interactive=False, | |
| height=200, | |
| ) | |
| output_3d = gr.Model3D( | |
| label="3-D Shape (GLB)", | |
| height=400, | |
| clear_color=[0.9, 0.9, 0.9, 1.0], | |
| ) | |
| download_file = gr.File(label="Download GLB") | |
| gr.Markdown( | |
| """ | |
| --- | |
| **Tips** | |
| - Works best with clean character art on a plain or transparent background. | |
| - Lower octree resolution (128-192) is faster and still looks great for most art. | |
| - Model: Hunyuan3D-DiT-v2-mini-Turbo - 0.6B parameters, step-distilled. | |
| """ | |
| ) | |
| generate_btn.click( | |
| fn=run, | |
| inputs=[input_image, seed, steps, octree_res], | |
| outputs=[output_3d, preview_img, download_file], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=5).launch() | |