import gradio as gr import os import sys import argparse import numpy as np import trimesh from pathlib import Path import torch import pytorch_lightning as pl import spaces sys.path.append('P3-SAM') from demo.auto_mask import AutoMask sys.path.append('XPart') from partgen.partformer_pipeline import PartFormerPipeline from partgen.utils.misc import get_config_from_file automask = AutoMask() def _load_pipeline(): pl.seed_everything(2026, workers=True) cfg_path = str(Path(__file__).parent / "XPart/partgen/config" / "infer.yaml") config = get_config_from_file(cfg_path) assert hasattr(config, "ckpt") or hasattr( config, "ckpt_path" ), "ckpt or ckpt_path must be specified in config" pipeline = PartFormerPipeline.from_pretrained( model_path="tencent/Hunyuan3D-Part", verbose=True, ) device = "cuda" pipeline.to(device=device, dtype=torch.float32) return pipeline _PIPELINE = _load_pipeline() output_path = 'P3-SAM/results/gradio' os.makedirs(output_path, exist_ok=True) def is_supported_3d_file(filename): # 获取文件扩展名(小写),并去除开头的点 ext = os.path.splitext(filename)[1].lower() return ext in ['.glb', '.ply', '.obj'] @spaces.GPU def segment(mesh_path, postprocess=True, postprocess_threshold=0.95, seed=42): if mesh_path is None: gr.Warning("No Input Mesh") return None, None if not is_supported_3d_file(mesh_path): gr.Warning("Only support glb ply obj.") return None, None mesh = trimesh.load(mesh_path, force='mesh', process=False) aabb, face_ids, mesh = automask.predict_aabb(mesh, seed=seed, is_parallel=False, post_process=postprocess, threshold=postprocess_threshold) color_map = {} unique_ids = np.unique(face_ids) for i in unique_ids: if i == -1: continue part_color = np.random.rand(3) * 255 color_map[i] = part_color face_colors = [] for i in face_ids: if i == -1: face_colors.append([0, 0, 0]) else: face_colors.append(color_map[i]) face_colors = np.array(face_colors).astype(np.uint8) mesh_save = mesh.copy() mesh_save.visual.face_colors = face_colors file_path = os.path.join(output_path, 'segment_mesh.glb') mesh_save.export(file_path) face_id_save_path = os.path.join(output_path, 'face_id.npy') np.save(face_id_save_path, face_ids) gr_state = [(aabb, mesh_path)] return file_path, face_id_save_path, gr_state @spaces.GPU(duration=150) def generate(mesh_path, seed=42, gr_state=None): if mesh_path is None: gr.Warning("No Input Mesh") gr_state[0] = (None, None) return None, None, None if gr_state[0][0] is None or mesh_path != gr_state[0][1]: gr.Warning("Please segment the mesh first") return None, None, None aabb = gr_state[0][0] # Ensure deterministic behavior per request try: pl.seed_everything(int(seed), workers=True) except Exception: pl.seed_everything(2026, workers=True) additional_params = {"output_type": "trimesh"} obj_mesh, (out_bbox, mesh_gt_bbox, explode_object) = _PIPELINE( mesh_path=mesh_path, aabb=aabb, octree_resolution=512, **additional_params, ) # Export all results to temporary files for Gradio Model3D obj_path = os.path.join(output_path, 'obj_mesh.glb') out_bbox_path = os.path.join(output_path, 'out_bbox.glb') explode_path = os.path.join(output_path, 'explode.glb') obj_mesh.export(obj_path) out_bbox.export(out_bbox_path) explode_object.export(explode_path) return obj_path, out_bbox_path, explode_path with gr.Blocks() as demo: gr.Markdown( ''' # ☯️ Hunyuan3D Part:P3-SAM&XPart This demo allows you to generate parts given a 3D model using Hunyuan3D-Part. First segment the 3D model using P3-SAM and then generate parts using XPart. Please upload glb ply or obj 3D model files. Our examples are at the bottoms. ''' ) with gr.Row(): with gr.Column(): # P3-SAM gr.Markdown( ''' ## P3-SAM: Native 3D Part Segmentation [Paper](https://arxiv.org/abs/2509.06784) | [Project Page](https://murcherful.github.io/P3-SAM/) | [Code](https://github.com/Tencent-Hunyuan/Hunyuan3D-Part/P3-SAM/) | [Model](https://huggingface.co/tencent/Hunyuan3D-Part) This is a demo of P3-SAM, a native 3D part segmentation method that can segment a mesh into different parts. Input a mesh and push the "Segment" button to get the segmentation results. ''' ) p3sam_button = gr.Button("Segment") p3sam_input = gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="Input Mesh") p3sam_output = gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="Segmentation Result") p3sam_face_id_output = gr.File(label='Face ID') p3sam_postprocess = gr.Checkbox(value=True, label="Post-processing") p3sam_postprocess_threshold = gr.Number(value=0.95, label="Post-processing Threshold") p3sam_seed = gr.Number(value=42, label="Random Seed") gr.Markdown( ''' P3-SAM will clean your mesh. To get face-aligned labels, you can download the "Segmentation Result" and "Face ID". You can also use the "Connectivity" and "Post-processing" options to control the behavior of the algorithm. The "Post-processing" will merge the small parts according to the threshold. The smaller the threshold, the more parts will be merged. ''' ) image_dump = gr.Image(label="Ref Image", visible=False) with gr.Column(): # XPart gr.Markdown( ''' ## XPart: High-fidelity and Structure-coherent Shapede Composition [Paper](https://arxiv.org/abs/2509.08643) | [Project Page](https://yanxinhao.github.io/Projects/X-Part/) | [Code](https://github.com/Tencent-Hunyuan/Hunyuan3D-Part/XPart/) | [Model](https://huggingface.co/tencent/Hunyuan3D-Part) This is a demo of the lite version of XPart, a high-fidelity and structure-coherent shape-decomposition method that can generate parts from a 3D model. Input a mesh, segment it using P3-SAM on the left, and push the "Generate" button to get the generated parts. ''' ) xpart_button = gr.Button("Generate") xpart_output = gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="Generated Parts") xpart_output_bbox = gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="Gnerated Parts with BBox") xpart_output_exploded = gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="Exploded Object") xpart_seed = gr.Number(value=42, label="Random Seed") gr_state = gr.State(value=[(None, None)]) with gr.Row(): gr.Examples(examples=[ ['P3-SAM/demo/assets/Female_Warrior.png' , 'P3-SAM/demo/assets/Female_Warrior.glb' ], ['P3-SAM/demo/assets/Suspended_Island.png' , 'P3-SAM/demo/assets/Suspended_Island.glb' ], ['P3-SAM/demo/assets/Beetle_Car.png' , 'P3-SAM/demo/assets/Beetle_Car.glb' ], ['XPart/data/Koi_Fish.png' , 'XPart/data/Koi_Fish.glb' ], ['XPart/data/Motorcycle.png' , 'XPart/data/Motorcycle.glb' ], ['XPart/data/Gundam.png' , 'XPart/data/Gundam.glb' ], ['XPart/data/Computer_Desk.png' , 'XPart/data/Computer_Desk.glb' ], ['XPart/data/Coffee_Machine.png' , 'XPart/data/Coffee_Machine.glb' ], ], inputs = [image_dump, p3sam_input], ) p3sam_button.click(segment, inputs=[p3sam_input, p3sam_postprocess, p3sam_postprocess_threshold, p3sam_seed], outputs=[p3sam_output, p3sam_face_id_output, gr_state]) xpart_button.click(generate, inputs=[p3sam_input, xpart_seed, gr_state], outputs=[xpart_output, xpart_output_bbox, xpart_output_exploded]) if __name__ == '__main__': demo.launch()