import gradio as gr import os import tempfile import uuid import time import shutil import logging from pathlib import Path from PIL import Image logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) logger = logging.getLogger("TRELLIS-3D") TRELLIS_SPACE = "microsoft/TRELLIS.2" TEMP_DIR = os.path.join(tempfile.gettempdir(), "trellis_3d") os.makedirs(TEMP_DIR, exist_ok=True) FALLBACK_3D_SPACES = [ "TencentARC/InstantMesh", "sudo-ai/zero123plus", ] def text_to_image(prompt, seed=0): from gradio_client import Client logger.info(f"Generating image from text: {prompt}") spaces = [ { "space": "stabilityai/stable-diffusion-3.5-large-turbo", "args": [prompt, "", seed, True, 1024, 1024, 4, 0], "endpoint": "/infer" }, { "space": "black-forest-labs/FLUX.1-schnell", "args": [prompt, seed, True, 1024, 1024, 4], "endpoint": "/infer" }, ] for space_config in spaces: try: logger.info(f"Trying image gen: {space_config['space']}") img_client = Client(space_config["space"]) result = img_client.predict( *space_config["args"], api_name=space_config["endpoint"] ) img_path = None if isinstance(result, str) and os.path.exists(result): img_path = result elif isinstance(result, tuple) and len(result) > 0: first = result[0] if isinstance(first, str) and os.path.exists(first): img_path = first elif isinstance(first, dict) and "path" in first: img_path = first["path"] elif isinstance(result, dict) and "path" in result: img_path = result["path"] elif hasattr(result, "path"): img_path = result.path if img_path and os.path.exists(img_path): logger.info(f"Image generated: {img_path}") return img_path except Exception as e: logger.warning(f"{space_config['space']} failed: {e}") continue return None def generate_3d_from_image( image_path, seed=0, randomize_seed=True, resolution="1024", ss_guidance_strength=7.5, ss_guidance_rescale=0.7, ss_sampling_steps=12, ss_rescale_t=5.0, shape_slat_guidance_strength=7.5, shape_slat_guidance_rescale=0.5, shape_slat_sampling_steps=12, shape_slat_rescale_t=3.0, tex_slat_guidance_strength=1.0, tex_slat_guidance_rescale=0.0, tex_slat_sampling_steps=12, tex_slat_rescale_t=3.0, decimation_target=300000, texture_size=2048 ): from gradio_client import Client, handle_file logger.info("Starting 3D generation pipeline") logger.info(f"Image: {image_path}") max_retries = 5 retry_delays = [10, 30, 60, 90, 120] for attempt in range(max_retries): try: if attempt > 0: wait_time = retry_delays[min(attempt, len(retry_delays) - 1)] logger.info( f"Retry {attempt + 1}/{max_retries} " f"after {wait_time}s wait..." ) time.sleep(wait_time) client = Client(TRELLIS_SPACE) logger.info("Connected to TRELLIS.2") logger.info("Step 1/5: Starting session...") try: client.predict(api_name="/start_session") logger.info("Session started") except Exception as e: logger.warning(f"Start session note: {e}") logger.info("Step 2/5: Preprocessing image...") processed_path = image_path try: preprocessed = client.predict( handle_file(image_path), api_name="/preprocess_image" ) logger.info(f"Preprocessed: {preprocessed}") if preprocessed: if isinstance(preprocessed, dict) and "path" in preprocessed: processed_path = preprocessed["path"] elif isinstance(preprocessed, str) and os.path.exists(preprocessed): processed_path = preprocessed elif hasattr(preprocessed, "path"): processed_path = preprocessed.path except Exception as e: logger.warning(f"Preprocess note: {e}") logger.info("Step 3/5: Getting seed...") actual_seed = seed try: actual_seed = client.predict( randomize_seed, seed, api_name="/get_seed" ) logger.info(f"Seed: {actual_seed}") except Exception as e: logger.warning(f"Seed note: {e}") logger.info("Step 4/5: Generating 3D (1-3 min)...") if isinstance(processed_path, str): image_input = handle_file(processed_path) else: image_input = processed_path preview_result = client.predict( image_input, actual_seed, resolution, ss_guidance_strength, ss_guidance_rescale, ss_sampling_steps, ss_rescale_t, shape_slat_guidance_strength, shape_slat_guidance_rescale, shape_slat_sampling_steps, shape_slat_rescale_t, tex_slat_guidance_strength, tex_slat_guidance_rescale, tex_slat_sampling_steps, tex_slat_rescale_t, api_name="/image_to_3d" ) logger.info(f"3D result: {str(preview_result)[:300]}") logger.info("Step 5/5: Extracting GLB...") glb_result = client.predict( decimation_target, texture_size, api_name="/extract_glb" ) logger.info(f"GLB result: {glb_result}") glb_path = extract_glb_path(glb_result) if glb_path and os.path.exists(glb_path): file_id = uuid.uuid4().hex[:8] output_path = os.path.join(TEMP_DIR, f"model_{file_id}.glb") shutil.copy2(glb_path, output_path) logger.info(f"GLB saved: {output_path}") return output_path logger.error(f"No GLB from: {glb_result}") return None except Exception as e: error_msg = str(e) logger.error(f"Attempt {attempt + 1} failed: {error_msg}") is_quota_error = any( phrase in error_msg.lower() for phrase in [ "gpu quota", "exceeded", "queue", "too many", "rate limit", "capacity", "busy" ] ) if is_quota_error and attempt < max_retries - 1: logger.info("GPU quota issue. Will retry...") continue elif is_quota_error: raise Exception( "GPU quota exceeded on TRELLIS. " "The free GPU is busy right now. " "Please try again in 2-5 minutes. " "Tip: Use resolution 512 for faster processing." ) else: raise return None def extract_glb_path(glb_result): if glb_result is None: return None if isinstance(glb_result, tuple): for item in glb_result: path = extract_single_path(item) if path: return path return extract_single_path(glb_result) def extract_single_path(item): if item is None: return None if isinstance(item, str) and os.path.exists(item): return item if isinstance(item, dict) and "path" in item: if os.path.exists(item["path"]): return item["path"] if hasattr(item, "path"): if isinstance(item.path, str) and os.path.exists(item.path): return item.path return None def handle_image_to_3d( image, seed, randomize_seed, resolution, ss_guidance_strength, ss_guidance_rescale, ss_sampling_steps, ss_rescale_t, shape_slat_guidance_strength, shape_slat_guidance_rescale, shape_slat_sampling_steps, shape_slat_rescale_t, tex_slat_guidance_strength, tex_slat_guidance_rescale, tex_slat_sampling_steps, tex_slat_rescale_t, decimation_target, texture_size, progress=gr.Progress() ): if image is None: raise gr.Error("Please upload an image!") start_time = time.time() progress(0.05, desc="Preparing image...") img_id = uuid.uuid4().hex[:8] img_path = os.path.join(TEMP_DIR, f"input_{img_id}.png") try: if isinstance(image, str) and os.path.exists(image): img_path = image elif hasattr(image, "save"): image.save(img_path, "PNG") else: raise gr.Error("Invalid image format") except gr.Error: raise except Exception as e: raise gr.Error(f"Image error: {e}") try: progress(0.1, desc="Connecting to TRELLIS (free GPU)...") progress(0.15, desc="Generating 3D... This takes 1-3 min. If GPU is busy it will auto-retry...") glb_path = generate_3d_from_image( image_path=img_path, seed=seed, randomize_seed=randomize_seed, resolution=str(int(resolution)), ss_guidance_strength=ss_guidance_strength, ss_guidance_rescale=ss_guidance_rescale, ss_sampling_steps=ss_sampling_steps, ss_rescale_t=ss_rescale_t, shape_slat_guidance_strength=shape_slat_guidance_strength, shape_slat_guidance_rescale=shape_slat_guidance_rescale, shape_slat_sampling_steps=shape_slat_sampling_steps, shape_slat_rescale_t=shape_slat_rescale_t, tex_slat_guidance_strength=tex_slat_guidance_strength, tex_slat_guidance_rescale=tex_slat_guidance_rescale, tex_slat_sampling_steps=tex_slat_sampling_steps, tex_slat_rescale_t=tex_slat_rescale_t, decimation_target=decimation_target, texture_size=texture_size ) duration = time.time() - start_time if glb_path and os.path.exists(glb_path): file_size = os.path.getsize(glb_path) / (1024 * 1024) status = ( f"Done! Generated in {duration:.1f}s | " f"File size: {file_size:.1f} MB" ) progress(1.0, desc="Done!") return glb_path, glb_path, status else: raise gr.Error("Generation completed but no GLB file was produced.") except gr.Error: raise except Exception as e: logger.error(f"Error: {e}", exc_info=True) raise gr.Error(f"Generation failed: {str(e)}") def handle_text_to_3d( prompt, seed, randomize_seed, resolution, ss_guidance_strength, ss_guidance_rescale, ss_sampling_steps, ss_rescale_t, shape_slat_guidance_strength, shape_slat_guidance_rescale, shape_slat_sampling_steps, shape_slat_rescale_t, tex_slat_guidance_strength, tex_slat_guidance_rescale, tex_slat_sampling_steps, tex_slat_rescale_t, decimation_target, texture_size, progress=gr.Progress() ): if not prompt or not prompt.strip(): raise gr.Error("Please enter a text prompt!") start_time = time.time() progress(0.05, desc="Step 1: Generating image from text...") img_path = text_to_image(prompt, seed) if img_path is None: raise gr.Error( "Could not generate image from text. " "The image AI spaces might be busy. " "Try the Image to 3D tab instead - " "generate an image with any AI tool and upload it." ) progress(0.3, desc="Step 2: Image ready! Generating 3D model...") try: glb_path = generate_3d_from_image( image_path=img_path, seed=seed, randomize_seed=randomize_seed, resolution=str(int(resolution)), ss_guidance_strength=ss_guidance_strength, ss_guidance_rescale=ss_guidance_rescale, ss_sampling_steps=ss_sampling_steps, ss_rescale_t=ss_rescale_t, shape_slat_guidance_strength=shape_slat_guidance_strength, shape_slat_guidance_rescale=shape_slat_guidance_rescale, shape_slat_sampling_steps=shape_slat_sampling_steps, shape_slat_rescale_t=shape_slat_rescale_t, tex_slat_guidance_strength=tex_slat_guidance_strength, tex_slat_guidance_rescale=tex_slat_guidance_rescale, tex_slat_sampling_steps=tex_slat_sampling_steps, tex_slat_rescale_t=tex_slat_rescale_t, decimation_target=decimation_target, texture_size=texture_size ) duration = time.time() - start_time if glb_path and os.path.exists(glb_path): file_size = os.path.getsize(glb_path) / (1024 * 1024) status = ( f"Done! Generated in {duration:.1f}s | " f"Prompt: {prompt} | " f"File: {file_size:.1f} MB" ) progress(1.0, desc="Done!") return img_path, glb_path, glb_path, status else: raise gr.Error("3D generation completed but no GLB file produced.") except gr.Error: raise except Exception as e: logger.error(f"Text to 3D error: {e}", exc_info=True) raise gr.Error(f"Failed: {str(e)}") def check_status(): try: from gradio_client import Client start = time.time() client = Client(TRELLIS_SPACE) connect_time = time.time() - start api_str = client.view_api(return_format="str") return ( "## Connected to TRELLIS.2\n\n" f"**Space:** `{TRELLIS_SPACE}`\n\n" f"**Connection time:** {connect_time:.1f}s\n\n" "**Status:** Online and Ready\n\n" "### How it works (all FREE):\n\n" "| Step | Endpoint | What it does |\n" "|------|----------|-------------|\n" "| 1 | /start_session | Initialize |\n" "| 2 | /preprocess_image | Clean image |\n" "| 3 | /get_seed | Prepare seed |\n" "| 4 | /image_to_3d | Generate 3D |\n" "| 5 | /extract_glb | Get GLB file |\n\n" "### Tips for free GPU:\n" "- Use resolution **512** for fastest results\n" "- If GPU quota error, wait 2-5 min and retry\n" "- Auto-retry is built in (up to 5 attempts)\n" "- Off-peak hours (night/early morning) work best\n\n" "### Raw API:\n" f"```\n{api_str}\n```" ) except Exception as e: return ( "## Connection Failed\n\n" f"**Error:** `{str(e)}`\n\n" "**Possible reasons:**\n" "- Space is sleeping (wait 2-3 min)\n" "- At capacity (try again later)\n" "- Under maintenance\n\n" "Click Check Connection again to retry." ) def cleanup(): try: count = 0 for f in Path(TEMP_DIR).glob("*"): if f.is_file(): f.unlink() count += 1 return f"Cleaned {count} files" except Exception as e: return f"Error: {e}" css = """ .gradio-container { max-width: 1200px !important; margin: auto !important; } .gen-btn { background: linear-gradient(135deg, #6c5ce7, #00b894) !important; color: white !important; font-size: 16px !important; font-weight: 600 !important; border: none !important; border-radius: 10px !important; min-height: 50px !important; } .gen-btn:hover { transform: translateY(-2px) !important; box-shadow: 0 6px 20px rgba(108, 92, 231, 0.4) !important; } .header-area { text-align: center; padding: 15px 0; } .header-area h1 { background: linear-gradient(135deg, #6c5ce7, #00b894); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 2.2em !important; } .tip-box { background: #e8f5e9; border: 1px solid #4caf50; border-radius: 8px; padding: 10px 15px; margin: 8px 0; color: #2e7d32; font-size: 14px; } .warn-box { background: #fff3e0; border: 1px solid #ff9800; border-radius: 8px; padding: 10px 15px; margin: 8px 0; color: #e65100; font-size: 14px; } """ with gr.Blocks( title="TRELLIS 3D Generator", theme=gr.themes.Soft( primary_hue="purple", secondary_hue="green", neutral_hue="slate", font=gr.themes.GoogleFont("Inter"), ), css=css ) as demo: gr.HTML( '
' "Generate 3D models from images or text | 100% Free | Powered by Microsoft TRELLIS 2" "
" "