diff --git a/alpha_api.py b/alpha_api.py new file mode 100644 index 00000000..e69de29b diff --git a/api_models.py b/api_models.py index dd37989f..421dda0c 100644 --- a/api_models.py +++ b/api_models.py @@ -1,7 +1,7 @@ """ Pydantic models for Hunyuan3D API server. """ -from typing import Optional, Literal +from typing import Optional, Literal, Dict from pydantic import BaseModel, Field @@ -26,6 +26,62 @@ class GenerationRequest(BaseModel): ge=0, le=2**32-1 ) + octree_resolution: int = Field( # Can be changed + 256, + description="Resolution of the octree for mesh generation", + ge=64, + le=512 + ) + num_inference_steps: int = Field( + 5, + description="Number of inference steps for generation", + ge=1, + le=20 + ) + guidance_scale: float = Field( # Can be changed + 5.0, + description="Guidance scale for generation", + ge=0.1, + le=20.0 + ) + num_chunks: int = Field( + 8000, + description="Number of chunks for processing", + ge=1000, + le=20000 + ) + face_count: int = Field( # Can be changed + 40000, + description="Maximum number of faces for texture generation", + ge=1000, + le=100000 + ) + + +class MultiViewGenerationRequest(BaseModel): + """Request model for multi-view 3D generation API""" + images: Dict[str, str] = Field( + ..., + description="Dictionary of view images with keys: 'front', 'back', 'left', 'right'. At least one view must be provided.", + example={ + "front": "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAEElEQVR4nGP8z4AATAxEcQAz0QEHOoQ+uAAAAABJRU5ErkJggg==", + "back": "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAEElEQVR4nGP8z4AATAxEcQAz0QEHOoQ+uAAAAABJRU5ErkJggg==" + } + ) + remove_background: bool = Field( + True, + description="Whether to automatically remove background from input images" + ) + texture: bool = Field( + False, + description="Whether to generate textures for the 3D model" + ) + seed: int = Field( + 1234, + description="Random seed for reproducible generation", + ge=0, + le=2**32-1 + ) octree_resolution: int = Field( 256, description="Resolution of the octree for mesh generation", @@ -70,6 +126,10 @@ class StatusResponse(BaseModel): None, description="Base64 encoded generated model file (only when status is 'completed')" ) + initial_model_base64: Optional[str] = Field( + None, + description="Base64 encoded initial model file (only when status is 'completed')" + ) message: Optional[str] = Field( None, description="Error message (only when status is 'error')" diff --git a/api_server.py b/api_server.py index ad1f219f..cad24b85 100644 --- a/api_server.py +++ b/api_server.py @@ -50,7 +50,6 @@ worker = None model_semaphore = None - app = FastAPI( title=API_TITLE, description=API_DESCRIPTION, @@ -171,13 +170,14 @@ async def status(uid: str): #print(f"Checking files: {textured_file_path} ({os.path.exists(textured_file_path)}), {initial_file_path} ({os.path.exists(initial_file_path)})") # If textured file exists, generation is complete - if os.path.exists(textured_file_path): + if os.path.exists(textured_file_path) and os.path.exists(initial_file_path): try: base64_str = base64.b64encode(open(textured_file_path, 'rb').read()).decode() - response = {'status': 'completed', 'model_base64': base64_str} + base64_str_initial = base64.b64encode(open(initial_file_path, 'rb').read()).decode() + response = {'status': 'completed', 'model_base64': base64_str, 'initial_model_base64': base64_str_initial} return JSONResponse(response, status_code=200) except Exception as e: - logger.error(f"Error reading file {textured_file_path}: {e}") + logger.error(f"{e}") response = {'status': 'error', 'message': 'Failed to read generated file'} return JSONResponse(response, status_code=500) @@ -212,7 +212,6 @@ async def status(uid: str): SAVE_DIR = args.cache_path os.makedirs(SAVE_DIR, exist_ok=True) - model_semaphore = asyncio.Semaphore(args.limit_model_concurrency) worker = ModelWorker( diff --git a/docker/Dockerfile b/docker/Dockerfile index c08e1ade..23e55c80 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,7 @@ ENV PATH="/workspace/miniconda3/bin:${PATH}" # initialize conda RUN conda init bash + # Accept Anaconda TOS for required channels RUN conda tos accept --channel https://repo.anaconda.com/pkgs/main && \ conda tos accept --channel https://repo.anaconda.com/pkgs/r @@ -55,26 +56,40 @@ RUN conda install cuda -c nvidia/label/cuda-12.4.1 -y # Update libstdcxx-ng to fix compatibility issues (auto-confirm) RUN conda install -c conda-forge libstdcxx-ng -y +RUN apt install -y python3.10-venv python3.10-distutils python3.10-dev build-essential cmake +RUN python3.10 -m venv venv310 +RUN venv310/bin/pip install --upgrade pip setuptools wheel + +RUN pip install --upgrade pip setuptools wheel + RUN pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu124 +# RUN pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu118 + +RUN pip install bpy==4.0 --extra-index-url https://download.blender.org/pypi/ + # Clone Hunyuan3D-2.1 repository -RUN git clone https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1.git +RUN git clone https://github.com/AryanGrutto/Hunyuan3D-2.1.git # Install Python dependencies from modified requirements.txt RUN pip install -r Hunyuan3D-2.1/requirements.txt # Install custom_rasterizer RUN cd /workspace/Hunyuan3D-2.1/hy3dpaint/custom_rasterizer && \ - # Set compilation environment variables export TORCH_CUDA_ARCH_LIST="6.0;6.1;7.0;7.5;8.0;8.6;8.9;9.0" && \ export CUDA_NVCC_FLAGS="-allow-unsupported-compiler" && \ - # Install with editable mode - pip install -e . + pip install --no-build-isolation . # Install DifferentiableRenderer -RUN cd /workspace/Hunyuan3D-2.1/hy3dpaint/DifferentiableRenderer && \ +RUN cd /workspace/Hunyuan3D-2.1/hy3dpaint && \ + mkdir -p hy3dpaint/DifferentiableRenderer && \ + python3 DifferentiableRenderer/setup.py build_ext --inplace && \ + cd DifferentiableRenderer && \ bash compile_mesh_painter.sh +RUN wget https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P hy3dpaint/ckpt + + # Create ckpt folder in hy3dpaint and download RealESRGAN model RUN cd /workspace/Hunyuan3D-2.1/hy3dpaint && \ mkdir -p ckpt && \ @@ -100,14 +115,15 @@ RUN apt-get install -y libxi6 libgconf-2-4 libxkbcommon-x11-0 libsm6 libxext6 li RUN echo "conda activate hunyuan3d21" >> ~/.bashrc SHELL ["/bin/bash", "--login", "-c"] -#exposing 7860 port -EXPOSE 7860 +#exposing 8081 port for API server +EXPOSE 8081 # Cleanup RUN rm -f /workspace/*.zip && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* + # Set default command WORKDIR /workspace/Hunyuan3D-2.1 RUN mkdir gradio_cache diff --git a/gradio_app.py b/gradio_app.py index 46b219d4..4f809b3b 100644 --- a/gradio_app.py +++ b/gradio_app.py @@ -452,13 +452,13 @@ def shape_generation( def build_app(): - title = 'Hunyuan3D-2: High Resolution Textured 3D Assets Generation' + title = 'Introducing Alpha 2.0 Image to 3D Generation' if MV_MODE: - title = 'Hunyuan3D-2mv: Image to 3D Generation with 1-4 Views' + title = 'Alpha3D-2-mv: Image to 3D Generation with 1-4 Views' if 'mini' in args.subfolder: - title = 'Hunyuan3D-2mini: Strong 0.6B Image to Shape Generator' + title = 'Alpha3D-2mini: Strong 0.6B Image to Shape Generator' - title = 'Hunyuan-3D-2.1' + title = 'Alpha3D-2' if TURBO_MODE: title = title.replace(':', '-Turbo: Fast ') @@ -469,7 +469,7 @@ def build_app(): {title}
- Tencent Hunyuan3D Team + Alpha Intelligence Team
""" custom_css = """ @@ -486,7 +486,7 @@ def build_app(): """ - with gr.Blocks(theme=gr.themes.Base(), title='Hunyuan-3D-2.1', analytics_enabled=False, css=custom_css) as demo: + with gr.Blocks(theme=gr.themes.Base(), title='Alpha3d 2.0', analytics_enabled=False, css=custom_css) as demo: gr.HTML(title_html) with gr.Row(): @@ -624,8 +624,9 @@ def build_app(): ], outputs=[file_out, html_gen_mesh, stats, seed] ).then( - lambda: (gr.update(visible=False, value=False), gr.update(interactive=True), gr.update(interactive=True), - gr.update(interactive=False)), + lambda file_path: (gr.update(visible=False, value=False), gr.update(interactive=True), gr.update(interactive=True), + gr.update(value=file_path, interactive=True)), + inputs=[file_out], outputs=[export_texture, reduce_face, confirm_export, file_export], ).then( lambda: gr.update(selected='gen_mesh_panel'), @@ -651,8 +652,9 @@ def build_app(): ], outputs=[file_out, file_out2, html_gen_mesh, stats, seed] ).then( - lambda: (gr.update(visible=True, value=True), gr.update(interactive=False), gr.update(interactive=True), - gr.update(interactive=False)), + lambda file_path: (gr.update(visible=True, value=True), gr.update(interactive=False), gr.update(interactive=True), + gr.update(value=file_path, interactive=True)), + inputs=[file_out2], outputs=[export_texture, reduce_face, confirm_export, file_export], ).then( lambda: gr.update(selected='gen_mesh_panel'), @@ -754,6 +756,7 @@ def on_export_click(file_out, file_out2, file_type, CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) MV_MODE = 'mv' in args.model_path + # MV_MODE = True TURBO_MODE = 'turbo' in args.subfolder HTML_HEIGHT = 690 if MV_MODE else 650 @@ -761,7 +764,7 @@ def on_export_click(file_out, file_out2, file_type, HTML_OUTPUT_PLACEHOLDER = f"""
-

Welcome to Hunyuan3D!

+

Welcone to Alpha 2.0 powered by Alpha Intelligence

No mesh here.

@@ -862,4 +865,4 @@ def on_export_click(file_out, file_out2, file_type, torch.cuda.empty_cache() demo = build_app() app = gr.mount_gradio_app(app, demo, path="/") - uvicorn.run(app, host=args.host, port=args.port) + uvicorn.run(app, host=args.host, port=args.port) \ No newline at end of file diff --git a/hy3dpaint/DifferentiableRenderer/setup.py b/hy3dpaint/DifferentiableRenderer/setup.py new file mode 100644 index 00000000..f27581e2 --- /dev/null +++ b/hy3dpaint/DifferentiableRenderer/setup.py @@ -0,0 +1,22 @@ +import os +from setuptools import setup +from pybind11.setup_helpers import Pybind11Extension, build_ext +import pybind11 +import numpy + +this_dir = os.path.dirname(__file__) +source_file = os.path.join(this_dir, "mesh_inpaint_processor.cpp") + +ext = Pybind11Extension( + "hy3dpaint.DifferentiableRenderer.mesh_inpaint_processor", + [source_file], + include_dirs=[pybind11.get_include(), numpy.get_include()], + language="c++", + extra_compile_args=["-O3", "-std=c++17"], +) + +setup( + name="mesh_inpaint_processor_build", + ext_modules=[ext], + cmdclass={"build_ext": build_ext}, +) \ No newline at end of file diff --git a/model_worker.py b/model_worker.py index 854f1d01..11b07758 100644 --- a/model_worker.py +++ b/model_worker.py @@ -174,45 +174,167 @@ def generate(self, uid, params): logger.error(f"Shape generation failed: {e}") raise ValueError(f"Failed to generate 3D mesh: {str(e)}") - # Export initial mesh without texture + # Check if texture generation is requested + generate_texture = params.get('texture', False) + # Export initial mesh without texture initial_save_path = os.path.join(self.save_dir, f'{str(uid)}_initial.glb') mesh.export(initial_save_path) + #Shape generation is done + + if generate_texture: + # Generate textured mesh as obj ( as in demo ) + try: + output_mesh_path_obj = os.path.join(self.save_dir, f'{str(uid)}_texturing.obj') + textured_path_obj = self.paint_pipeline( + mesh_path=initial_save_path, + image_path=image, + output_mesh_path=output_mesh_path_obj, + save_glb=False + ) + logger.info("---Texture generation takes %s seconds ---" % (time.time() - start_time)) + logger.info(f"output_mesh_path: {output_mesh_path_obj} textured_path: {textured_path_obj}") + + # Convert textured OBJ to GLB using obj2gltf with PBR support + print("convert textured OBJ to GLB") + glb_path_textured = os.path.join(self.save_dir, f'{str(uid)}_texturing.glb') + quick_convert_with_obj2gltf(textured_path_obj, glb_path_textured) + # now rename glb_path to uid_textured.glb + print("done.") + final_save_path = os.path.join(self.save_dir, f'{str(uid)}_textured.glb') + os.rename(glb_path_textured, final_save_path) + print(f"final_save_path: {final_save_path}") + + + except Exception as e: + logger.error(f"Texture generation failed: {e}") + # Fall back to untextured mesh if texture generation fails + final_save_path = initial_save_path + logger.warning(f"Using untextured mesh as fallback: {final_save_path}") + else: + # Skip texture generation, use initial mesh as final output + logger.info("Skipping texture generation as requested") + final_save_path = initial_save_path + + if self.low_vram_mode: + torch.cuda.empty_cache() + + logger.info("---Total generation takes %s seconds ---" % (time.time() - start_time)) + # Alpha completion action calls the webhook to notify the client about the completion + + return final_save_path, uid + + @torch.inference_mode() + def generate_multiview(self, uid, params): + """ + Generate a 3D model from multiple view images. + + Args: + uid: Unique identifier for this generation task + params (dict): Generation parameters including images dict and options + + Returns: + tuple: (file_path, uid) - Path to generated file and task ID + """ + start_time = time.time() + logger.info(f"Generating 3D model from multiple views for uid: {uid}") - # Generate textured mesh as obj ( as in demo ) + # Handle input images + if 'images' in params: + images_dict = params["images"] + if not images_dict or len(images_dict) == 0: + raise ValueError("No input images provided") + + # Convert base64 images to PIL Images + images = {} + for view_name, image_b64 in images_dict.items(): + if view_name in ['front', 'back', 'left', 'right']: + images[view_name] = load_image_from_base64(image_b64) + else: + logger.warning(f"Unknown view name: {view_name}, skipping") + else: + raise ValueError("No input images provided") + + # Convert to RGBA and remove background if needed + for view_name, image in images.items(): + image = image.convert("RGBA") + if image.mode == "RGB": + images[view_name] = self.rembg(image) + + # Generate mesh from multiple views using the same approach as gradio app try: - output_mesh_path_obj = os.path.join(self.save_dir, f'{str(uid)}_texturing.obj') - textured_path_obj = self.paint_pipeline( - mesh_path=initial_save_path, - image_path=image, - output_mesh_path=output_mesh_path_obj, - save_glb=False + # Set up generator with seed + generator = torch.Generator() + generator = generator.manual_seed(int(params.get('seed', 1234))) + + # Call pipeline with multiple views (same as gradio app) + outputs = self.pipeline( + image=images, + num_inference_steps=params.get('num_inference_steps', 5), + guidance_scale=params.get('guidance_scale', 5.0), + generator=generator, + octree_resolution=params.get('octree_resolution', 256), + num_chunks=params.get('num_chunks', 8000), + output_type='mesh' ) - logger.info("---Texture generation takes %s seconds ---" % (time.time() - start_time)) - logger.info(f"output_mesh_path: {output_mesh_path_obj} textured_path: {textured_path_obj}") - # Use the textured GLB as the final output - #final_save_path = os.path.join(self.save_dir, f'{str(uid)}_textured.{file_type}') - #os.rename(output_mesh_path, final_save_path) - - # Convert textured OBJ to GLB using obj2gltf with PBR support - print("convert textured OBJ to GLB") - glb_path_textured = os.path.join(self.save_dir, f'{str(uid)}_texturing.glb') - quick_convert_with_obj2gltf(textured_path_obj, glb_path_textured) - # now rename glb_path to uid_textured.glb - print("done.") - final_save_path = os.path.join(self.save_dir, f'{str(uid)}_textured.glb') - os.rename(glb_path_textured, final_save_path) - print(f"final_save_path: {final_save_path}") - + # Extract mesh from outputs (same as gradio app) + from hy3dshape.pipelines import export_to_trimesh + mesh = export_to_trimesh(outputs)[0] + + logger.info("---Multiview shape generation takes %s seconds ---" % (time.time() - start_time)) except Exception as e: - logger.error(f"Texture generation failed: {e}") - # Fall back to untextured mesh if texture generation fails + logger.error(f"Multiview shape generation failed: {e}") + raise ValueError(f"Failed to generate 3D mesh from multiple views: {str(e)}") + + # Check if texture generation is requested + generate_texture = params.get('texture', False) + + # Export initial mesh without texture + initial_save_path = os.path.join(self.save_dir, f'{str(uid)}_initial.glb') + mesh.export(initial_save_path) + + if generate_texture: + # Generate textured mesh as obj ( as in demo ) + try: + output_mesh_path_obj = os.path.join(self.save_dir, f'{str(uid)}_texturing.obj') + # Pass all available views to texture pipeline for better quality + # The texture pipeline can handle multiple images and will use them for multiview texture generation + textured_path_obj = self.paint_pipeline( + mesh_path=initial_save_path, + image_path=list(images.values()), # Pass all views as a list + output_mesh_path=output_mesh_path_obj, + save_glb=False + ) + logger.info("---Multiview texture generation takes %s seconds ---" % (time.time() - start_time)) + logger.info(f"output_mesh_path: {output_mesh_path_obj} textured_path: {textured_path_obj}") + + # Convert textured OBJ to GLB using obj2gltf with PBR support + print("convert textured OBJ to GLB") + glb_path_textured = os.path.join(self.save_dir, f'{str(uid)}_texturing.glb') + quick_convert_with_obj2gltf(textured_path_obj, glb_path_textured) + # now rename glb_path to uid_textured.glb + print("done.") + final_save_path = os.path.join(self.save_dir, f'{str(uid)}_textured.glb') + os.rename(glb_path_textured, final_save_path) + print(f"final_save_path: {final_save_path}") + + + except Exception as e: + logger.error(f"Multiview texture generation failed: {e}") + # Fall back to untextured mesh if texture generation fails + final_save_path = initial_save_path + logger.warning(f"Using untextured mesh as fallback: {final_save_path}") + else: + # Skip texture generation, use initial mesh as final output + logger.info("Skipping texture generation as requested") final_save_path = initial_save_path - logger.warning(f"Using untextured mesh as fallback: {final_save_path}") if self.low_vram_mode: torch.cuda.empty_cache() - logger.info("---Total generation takes %s seconds ---" % (time.time() - start_time)) + logger.info("---Total multiview generation takes %s seconds ---" % (time.time() - start_time)) + + # Alpha completion action calls the webhook to notify the client about the completion + return final_save_path, uid \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d9f1e4dd..787ab093 100644 --- a/requirements.txt +++ b/requirements.txt @@ -53,7 +53,7 @@ psutil==6.0.0 cupy-cuda12x==13.4.1 # Blender -bpy==4.0 +bpy # ONNX Runtime onnxruntime==1.16.3