| """ |
| 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") |
|
|
| |
| 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) |
|
|
| |
| 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()) |
|
|
| |
| if isinstance(image, Image.Image): |
| ip = out / f"input_{ts}.png" |
| image.save(ip) |
| image_path = str(ip) |
| else: |
| image_path = image |
|
|
| |
| 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: |
| from gradio_client import Client |
| client = Client(target, timeout=300) |
| |
| 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: |
| |
| |
| model_path = str(out / f"model_{ts}.glb") |
| |
| Path(model_path).write_bytes(b"") |
| model_used = "local_placeholder" |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| demo = build_ui() |
| gradio_app = demo |
|
|
| 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) |