import gradio as gr import runpod import base64 import os import io import zipfile import tempfile import shutil import trimesh from PIL import Image import requests # Configuration RUNPOD_API_KEY = os.environ.get("RUNPOD_API_KEY", "") RUNPOD_ENDPOINT_ID = os.environ.get("RUNPOD_ENDPOINT_ID", "") def convert_obj_to_glb(obj_path, texture_path, output_glb_path): """Convert OBJ + texture to GLB format""" try: # Load the mesh mesh = trimesh.load(obj_path, process=False) # Load texture image if os.path.exists(texture_path): texture_image = Image.open(texture_path) # Create a material with the texture material = trimesh.visual.material.PBRMaterial( baseColorTexture=texture_image, baseColorFactor=[1.0, 1.0, 1.0, 1.0], ) # Apply material to mesh if hasattr(mesh, 'visual'): mesh.visual.material = material # Export to GLB mesh.export(output_glb_path, file_type='glb') print(f"✅ Converted to GLB: {output_glb_path}") return output_glb_path except Exception as e: print(f"❌ Error converting to GLB: {str(e)}") # Fallback: try simple conversion without texture try: mesh = trimesh.load(obj_path) mesh.export(output_glb_path, file_type='glb') return output_glb_path except Exception as e2: print(f"❌ Fallback conversion also failed: {str(e2)}") raise def generate_3d_model(image, api_key, endpoint_id, progress=gr.Progress()): """Generate 3D model from image using RunPod endpoint""" # Validate inputs if not api_key: return None, None, "❌ Please provide RunPod API Key" if not endpoint_id: return None, None, "❌ Please provide RunPod Endpoint ID" if image is None: return None, None, "❌ Please upload an image" try: progress(0.1, desc="Encoding image...") # Convert PIL Image to base64 buffered = io.BytesIO() image.save(buffered, format="PNG") image_base64 = base64.b64encode(buffered.getvalue()).decode() progress(0.2, desc="Connecting to RunPod...") # Set API key and get endpoint runpod.api_key = api_key endpoint = runpod.Endpoint(endpoint_id) progress(0.3, desc="Sending request to RunPod (this may take a few minutes)...") # Run inference run_request = endpoint.run({ "input": { "image": image_base64 } }) progress(0.4, desc="Waiting for 3D model generation...") # Wait for result with timeout result = run_request.output(timeout=600) # 10 minute timeout progress(0.7, desc="Processing results...") # Check for errors if "error" in result: error_msg = result.get("error", "Unknown error") return None, None, f"❌ RunPod Error: {error_msg}" if "output" not in result: return None, None, "❌ No output received from RunPod" progress(0.8, desc="Decoding output...") # Decode the zip file zip_data = base64.b64decode(result["output"]) # Create temporary directory temp_dir = tempfile.mkdtemp() zip_path = os.path.join(temp_dir, "output.zip") # Save zip file with open(zip_path, "wb") as f: f.write(zip_data) # Extract zip extract_dir = os.path.join(temp_dir, "extracted") os.makedirs(extract_dir, exist_ok=True) with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(extract_dir) progress(0.9, desc="Converting to GLB format...") # Find the OBJ and texture files obj_file = None texture_file = None for file in os.listdir(extract_dir): if file.endswith('.obj') and 'textured' in file: obj_file = os.path.join(extract_dir, file) elif file.endswith('.jpg') and 'textured' in file and 'metallic' not in file and 'roughness' not in file: texture_file = os.path.join(extract_dir, file) if not obj_file: # Fallback: find any OBJ file for file in os.listdir(extract_dir): if file.endswith('.obj'): obj_file = os.path.join(extract_dir, file) break if not texture_file: # Fallback: find any texture file for file in os.listdir(extract_dir): if file.endswith('.jpg') or file.endswith('.png'): texture_file = os.path.join(extract_dir, file) break if not obj_file: return None, zip_path, "❌ No OBJ file found in output" # Convert to GLB glb_path = os.path.join(temp_dir, "model.glb") convert_obj_to_glb(obj_file, texture_file, glb_path) progress(1.0, desc="Complete!") # Return GLB path and zip path return glb_path, zip_path, "✅ 3D model generated successfully!" except Exception as e: error_msg = f"❌ Error: {str(e)}" print(error_msg) import traceback traceback.print_exc() return None, None, error_msg # Create Gradio interface with gr.Blocks(title="Image to 3D Model", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🎨 Image to 3D Model Generator Generate textured 3D models from images using AI. Upload an image and get a downloadable 3D model! **Powered by Hunyuan3D-2.1 via RunPod Serverless** """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📸 Input") # API Configuration with gr.Accordion("⚙️ RunPod Configuration", open=False): api_key_input = gr.Textbox( label="RunPod API Key", placeholder="Enter your RunPod API key", type="password", value=RUNPOD_API_KEY ) endpoint_id_input = gr.Textbox( label="RunPod Endpoint ID", placeholder="Enter your endpoint ID", value=RUNPOD_ENDPOINT_ID ) gr.Markdown(""" Get your API key and Endpoint ID from [RunPod Dashboard](https://www.runpod.io/console/serverless) You can also set them as environment variables: - `RUNPOD_API_KEY` - `RUNPOD_ENDPOINT_ID` """) # Image input image_input = gr.Image( label="Upload Image", type="pil", sources=["upload", "webcam", "clipboard"] ) # Example images gr.Examples( examples=[ "examples/chair.jpg", "examples/shoe.jpg", "examples/vase.jpg", ], inputs=image_input, label="Example Images (add your own in /examples folder)" ) # Generate button generate_btn = gr.Button("🚀 Generate 3D Model", variant="primary", size="lg") # Status message status_output = gr.Textbox(label="Status", interactive=False) with gr.Column(scale=1): gr.Markdown("### 🎯 Output") # 3D Model viewer model_output = gr.Model3D( label="Generated 3D Model", clear_color=[0.1, 0.1, 0.1, 1.0], height=500 ) # Download button download_output = gr.File( label="📦 Download Files (ZIP)", interactive=False ) gr.Markdown(""" ### 📝 Output Files Include: - `demo_textured.obj` - 3D model geometry - `demo_textured.jpg` - Color texture - `demo_textured.mtl` - Material definition - `demo_textured_metallic.jpg` - Metallic map - `demo_textured_roughness.jpg` - Roughness map ### 💡 Tips: - Works best with clear object images on white/transparent background - Single object works better than complex scenes - Processing takes 2-5 minutes depending on server load - You can import the GLB file into Blender, Unity, Unreal Engine, etc. """) # Event handler generate_btn.click( fn=generate_3d_model, inputs=[image_input, api_key_input, endpoint_id_input], outputs=[model_output, download_output, status_output] ) gr.Markdown(""" --- ### 🔧 Troubleshooting **Error: No API key/endpoint ID** - Set them in the configuration accordion above - Or set as environment variables in Hugging Face Space settings **Error: Request timeout** - The endpoint might be cold-starting (first request takes longer) - Try again after a few minutes **Error: GPU out of memory** - The RunPod endpoint might need more VRAM - Try using a larger GPU tier in RunPod settings **Model looks incorrect** - Try using a clearer image with better lighting - Ensure the object is centered and on a plain background ### 📚 Resources - [RunPod Serverless Docs](https://docs.runpod.io/serverless/overview) - [Hunyuan3D GitHub](https://github.com/Tencent/Hunyuan3D) """) # Launch the app if __name__ == "__main__": demo.launch( share=False, server_name="0.0.0.0", server_port=7860 )