""" forge-3d Brick — Image (and text) → 3D + LODs + renders + analysis (Wave 1/2). Design: orchestration + adapter pattern (inspired directly by 3d-creator-suite src/models/base.py + health). - Local / Comfy / TripoSR primary (user 4070 Ti path, zero cost). - Health-checked cloud fallbacks (our controlled or stable public). - CPU-friendly mode for pure orchestration (like the past 3d-creator-suite Space). - Outputs glb + manifest + multi-view previews (for reverse describe in visuals brick). - @spaces.GPU on heavy paths; ZeroGPU deployable. Scaffold: functional UI + API + manifest. Real adapters + local Comfy wiring next. """ from __future__ import annotations import os from pathlib import Path from datetime import datetime import gradio as gr import spaces import logging from PIL import Image logging.basicConfig(level=logging.INFO) logger = logging.getLogger("forge-3d") # Robust import for local workspace + HF Space deployment (common is vendored on push) import sys from pathlib import Path HERE = Path(__file__).resolve().parent for candidate in (HERE.parent, HERE): if (candidate / "common" / "manifest.py").exists(): sys.path.insert(0, str(candidate)) break try: from common.manifest import create_manifest from common.health import SpaceHealth except ImportError as e: raise ImportError( "Failed to import shared 'common' package (manifest.py / health.py).\n" "For local development: run ./install.sh from the forge-bricks/ root.\n" "For HF Spaces: re-run scripts/push_to_hf.py so it vendors common/." ) from e health = SpaceHealth(check_interval=180) # Stable public fallbacks (health checked). Prefer our controlled bricks/local first. TRIPOSG = "VAST-AI/TripoSG" HUNYUAN = "Tencent/Hunyuan3D-2" def _out_dir() -> Path: d = Path(os.environ.get("FORGE_BRICKS_OUTPUT", "./outputs/forge_3d")) d.mkdir(parents=True, exist_ok=True) return d @spaces.GPU(duration=180) def generate_3d( image: str | Image.Image, prompt: str = "", model_choice: str = "triposg", target_faces: int = 50000, progress=gr.Progress(), ) -> tuple[str, list[str], dict]: """Image (+ optional text) → 3D model + preview renders + manifest. Adapter style (see 3d-creator-suite base.ModelAdapter): health + local-first + fallback. """ progress(0, desc="Health + 3D generation...") out = _out_dir() ts = int(datetime.now().timestamp()) # 1. Ensure we have a filepath if isinstance(image, Image.Image): ip = out / f"input_{ts}.png" image.save(ip) image_path = str(ip) else: image_path = image # 2. Health + choose backend (real calls to open-source 3D models via their Spaces) target = TRIPOSG if model_choice == "triposg" else HUNYUAN api_name = "/generate" if "Tripo" in target else "/generation_all" ok = health.is_ok(target) model_used = "fallback" model_path = None if ok is not False: # try try: from gradio_client import Client client = Client(target, timeout=300) # Real call to the 3D generation Space (TripoSG / Hunyuan3D) res = client.predict(image_path, api_name=api_name) if res: if isinstance(res, str) and os.path.exists(res) and res.endswith((".glb", ".obj", ".gltf")): model_path = res elif isinstance(res, dict) and "path" in res: p = res["path"] if os.path.exists(p): model_path = p model_used = target.split("/")[-1] except Exception as e: logger.warning(f"Cloud 3D attempt to {target} failed: {e}") if not model_path: # Robust fallback: produce a tiny placeholder glb-like (real impl uses local TripoSR/Comfy or mesh gen) # For demo we just note the path; production will write a real glb via local adapter. model_path = str(out / f"model_{ts}.glb") # Touch a placeholder so the rest of the pipeline (manifest, Godot helper, daggr) can continue Path(model_path).write_bytes(b"") # real code will overwrite with valid glb model_used = "local_placeholder" # 3. Simple multi-view "renders" (placeholder images; real = render the glb or request from 3D Space) previews = [] for view in ["front", "side", "top"]: p = out / f"preview_{ts}_{view}.png" Image.new("RGB", (512, 512), (60, 70, 90)).save(p) previews.append(str(p)) manifest = create_manifest( name=Path(image_path).stem or "asset", type="model_3d", source_brick="forge-3d", prompt_or_desc=prompt or "image-to-3d", files={"model": model_path, "previews": ",".join(previews)}, params={"model_choice": model_choice, "target_faces": target_faces, "backend": model_used}, metadata={"note": "Scaffold — adapters + local Comfy primary coming; health used"}, commercial_ok=True, license_note="Depends on backend (TripoSG etc. — verify per use).", ) mpath = out / f"manifest_{ts}.json" manifest.save(mpath) progress(1.0) return model_path, previews, manifest.to_dict() def build_ui(): with gr.Blocks(title="Forge-3D") as demo: gr.Markdown("# 🧊 Forge-3D (image/text → 3D + previews + analysis hooks)\nRobust (health + adapters + local first). Pairs with forge-visuals for full bidir.") with gr.Row(): img = gr.Image(label="Input image (or concept)", type="filepath") txt = gr.Textbox(label="Optional text prompt (for some models)") choice = gr.Dropdown(["triposg", "hunyuan3d"], value="triposg", label="Backend preference") faces = gr.Slider(10000, 200000, value=50000, step=10000, label="Target faces (approx)") btn = gr.Button("Generate 3D", variant="primary") model = gr.File(label="3D Model (glb/obj)") previews = gr.Gallery(label="Multi-view previews (for describe/refine)") man = gr.JSON(label="Manifest (lineage ready)") btn.click(generate_3d, [img, txt, choice, faces], [model, previews, man]) gr.Markdown("API: `/generate_3d` (image, prompt, model_choice, target_faces). Use with daggr + health-aware routing.") return demo # Build at module level so that `demo` (and `gradio_app`) exist when the module is imported # (required for Hugging Face Spaces and for agent/MCP discovery). demo = build_ui() gradio_app = demo # alias for compatibility with tools/skills that expect `gradio_app` if __name__ == "__main__": health.start([TRIPOSG, HUNYUAN]) demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7861)), mcp_server=True)