import argparse import json import os import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[3] VENDOR = ROOT / "vendor" / "trellis-mac" ap = argparse.ArgumentParser() ap.add_argument("--input", action="append", default=[]) ap.add_argument("--outdir", required=True) ap.add_argument("--params", default="{}") a = ap.parse_args() p = json.loads(a.params) if not a.input: print("ERROR: no input image") sys.exit(1) if not (VENDOR / "generate.py").exists(): print(f"ERROR: trellis-mac not installed at {VENDOR}. Run scripts/install_trellis_mac.sh") sys.exit(1) # manifest no longer pins "python" (broke remote dispatch when the venv path # didn't exist on the worker) — resolve the vendor venv here instead. VENV_PY = VENDOR / ".venv" / "bin" / "python" if not VENV_PY.exists(): print(f"ERROR: no venv at {VENV_PY} — run scripts/install_trellis_mac.sh") sys.exit(1) outdir = Path(a.outdir) out_stem = outdir / f"trellis2_{Path(a.input[0]).stem}" cmd = [ str(VENV_PY), "generate.py", str(Path(a.input[0]).resolve()), "--pipeline-type", str(p.get("pipeline_type", "1024")), "--texture-size", str(p.get("texture_size", 2048)), "--seed", str(p.get("seed", 0)), "--output", str(out_stem.resolve()), ] if p.get("no_texture"): cmd.append("--no-texture") if p.get("steps"): cmd += ["--steps", str(p["steps"])] env = os.environ.copy() # xet chunked downloader fails on these BFL/Meta repos ("Unable to parse string # as hex hash value") — force the reliable HTTP path (same fix as flux_local). env["HF_HUB_DISABLE_XET"] = "1" # guard the torch + compiled-kernel OpenMP collision like sf3d does. env["KMP_DUPLICATE_LIB_OK"] = "TRUE" env.setdefault("OMP_NUM_THREADS", "1") env.setdefault("MKL_NUM_THREADS", "1") # Pre-bake simplify cap. The 200k generate.py default is a laptop-derived # mtlbvh guard that costs most of the mesh detail (2.2M-tri decode → 95k GLB # vs fal's 495k). 500k verified crash-free on the M3 Ultra Studio 2026-07-22. # The server passes only client-sent params (manifest defaults are UI-only), # so default here, sized by node RAM: big-memory Studios get the quality cap. if p.get("max_bake_faces") is not None: env["TRELLIS2_MAX_BAKE_FACES"] = str(p["max_bake_faces"]) else: mem_gb = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1e9 env.setdefault("TRELLIS2_MAX_BAKE_FACES", "500000" if mem_gb >= 96 else "200000") if p.get("baker"): env["TRELLIS2_BAKER"] = str(p["baker"]) # "auto" keeps o_voxel's baked-alpha BLEND detection (glass etc.); default # opaque — the Metal baker's noisy alpha turns solid hair into speckles. if p.get("alpha_mode"): env["TRELLIS2_ALPHA_MODE"] = str(p["alpha_mode"]) print("+", " ".join(cmd), flush=True) print("(first run downloads TRELLIS.2-4B + dinov3 + RMBG-2.0 weights)", flush=True) res = subprocess.run(cmd, cwd=str(VENDOR), stdout=sys.stdout, stderr=subprocess.STDOUT, env=env) if res.returncode != 0: sys.exit(res.returncode) # generate.py writes .glb (and often a .obj); collect any glb under outdir glbs = sorted(outdir.rglob("*.glb")) if not glbs: print("ERROR: trellis-mac produced no .glb") sys.exit(1) outputs = [{"path": str(glbs[0]), "name": f"trellis2_{Path(a.input[0]).stem}.glb", "meta": {"tool": "trellis_mac"}}] (outdir / "result.json").write_text(json.dumps({"outputs": outputs})) print("done:", glbs[0], flush=True)