"""Hunyuan3D-2.1 3D model generation.""" # CRITICAL: Import spaces BEFORE torch/CUDA packages import spaces import torch from pathlib import Path from gradio_client import Client, handle_file import httpx from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from core.config import HUNYUAN_SETTINGS, QualityPreset from utils.memory import MemoryManager class HunyuanGenerator: """Generates 3D models using Hunyuan3D-2.1.""" def __init__(self): self.memory_manager = MemoryManager() @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10), retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)) ) def _call_api(self, client: Client, **kwargs): """Call Hunyuan3D API with automatic retry.""" return client.predict(**kwargs) @spaces.GPU(duration=90) def generate( self, image_path: Path, preset: QualityPreset, output_dir: Path ) -> Path: """Generate 3D model from 2D image.""" try: print(f"[Hunyuan3D] Generating 3D model: {preset.name} quality") print(f"[Hunyuan3D] Input image: {image_path}") print(f"[Hunyuan3D] Settings: steps={preset.hunyuan_steps}, guidance={preset.hunyuan_guidance}, octree={preset.octree_resolution}") # Validate input image exists if not image_path.exists(): raise FileNotFoundError(f"Input image not found: {image_path}") # Connect to API print(f"[Hunyuan3D] Connecting to {HUNYUAN_SETTINGS['space_id']}...") client = Client( HUNYUAN_SETTINGS["space_id"], httpx_kwargs={ "timeout": httpx.Timeout( HUNYUAN_SETTINGS["timeout"], connect=HUNYUAN_SETTINGS["connect_timeout"] ) } ) print(f"[Hunyuan3D] Connected successfully") # Call API (returns tuple: file, output, mesh_stats, seed) print(f"[Hunyuan3D] Calling API with parameters...") result = self._call_api( client, image=handle_file(str(image_path)), mv_image_front=None, mv_image_back=None, mv_image_left=None, mv_image_right=None, steps=preset.hunyuan_steps, guidance_scale=preset.hunyuan_guidance, seed=1234, octree_resolution=preset.octree_resolution, check_box_rembg=True, num_chunks=preset.num_chunks, randomize_seed=True, api_name="/shape_generation" ) print(f"[Hunyuan3D] API call completed") # Extract GLB path from tuple response # API returns: (file, output, mesh_stats, seed) print(f"[Hunyuan3D] Raw result type: {type(result)}") print(f"[Hunyuan3D] Raw result length: {len(result) if isinstance(result, (tuple, list)) else 'N/A'}") if not isinstance(result, tuple): raise ValueError( f"Unexpected result type from Hunyuan3D API: {type(result)}. " f"Expected tuple of (file, output, mesh_stats, seed)." ) if len(result) != 4: raise ValueError( f"Unexpected result length from Hunyuan3D API: {len(result)}. " f"Expected 4 elements (file, output, mesh_stats, seed), got {len(result)}." ) # Extract GLB file path (first element) file_data, html_output, mesh_stats, used_seed = result print(f"[Hunyuan3D] file_data type: {type(file_data)}") print(f"[Hunyuan3D] mesh_stats: {mesh_stats}") print(f"[Hunyuan3D] used_seed: {used_seed}") # Extract path from file_data if file_data is None: raise ValueError( "Hunyuan3D API returned None for file. " "This usually means the generation failed on the server side. " "Possible causes:\n" " - Invalid image input\n" " - API timeout\n" " - Server overload\n" "Try again with a different image or quality setting." ) # Handle different file_data formats if isinstance(file_data, dict): print(f"[Hunyuan3D] file_data is dict with keys: {file_data.keys()}") if 'path' in file_data: glb_path = file_data['path'] elif 'value' in file_data: glb_path = file_data['value'] elif 'name' in file_data: glb_path = file_data['name'] else: raise ValueError( f"Unexpected dict format from Hunyuan3D API. " f"Keys: {list(file_data.keys())}" ) elif isinstance(file_data, str): glb_path = file_data else: raise ValueError( f"Unexpected file_data type: {type(file_data)}. " f"Expected dict or str." ) print(f"[Hunyuan3D] Extracted GLB path: {glb_path}") # Validate path exists if not glb_path or glb_path == "None": raise ValueError( "Hunyuan3D API returned invalid path. " "The generation may have failed on the server side." ) if not Path(glb_path).exists(): raise ValueError( f"GLB file not found at path: {glb_path}. " f"The file may not have been generated or saved correctly." ) print(f"[Hunyuan3D] Model generated: {glb_path}") # Cleanup del client import gc gc.collect() torch.cuda.empty_cache() return Path(glb_path) except Exception as e: import traceback error_details = traceback.format_exc() print(f"[Hunyuan3D] ERROR: {e}") print(f"[Hunyuan3D] Full traceback:\n{error_details}") # Provide helpful error message based on error type error_str = str(e).lower() if "quota" in error_str or "zerogpu" in error_str: raise RuntimeError( f"⚠️ Hunyuan3D Space is out of GPU quota.\n" f"This is a limitation of the free Hunyuan3D-2.1 Space.\n\n" f"Solutions:\n" f"1. Wait for quota reset (resets daily)\n" f"2. Try again in a few hours\n" f"3. Use a different time of day (less traffic)\n\n" f"Note: Your L4 GPU is only used for FLUX generation.\n" f"Hunyuan3D runs on an external space with quota limits." ) from e elif "list index out of range" in str(e) or "unexpected result" in error_str: raise ValueError( f"❌ Hunyuan3D API returned empty result.\n" f"This usually means:\n" f"1. The Hunyuan3D Space is overloaded\n" f"2. GPU quota exhausted\n" f"3. Invalid image input\n\n" f"Try again in a few minutes." ) from e elif "timeout" in error_str: raise TimeoutError( f"⏱️ Hunyuan3D generation timed out.\n" f"Try using a lower quality preset (Fast or Balanced)." ) from e elif "not found" in error_str or "404" in error_str: raise RuntimeError( f"❌ Hunyuan3D Space not accessible.\n" f"The tencent/Hunyuan3D-2.1 Space may be down or moved.\n" f"Check: https://huggingface.co/spaces/tencent/Hunyuan3D-2.1" ) from e else: raise RuntimeError( f"❌ Hunyuan3D generation failed: {e}\n" f"Check the Hunyuan3D Space status and try again." ) from e