Spaces:
Sleeping
Sleeping
| """Sandboxed execution of generator code -> validated STL/GLB mesh. | |
| Generated Python runs in a restricted namespace exposing only trimesh + numpy. | |
| On failure the stderr/exception is returned so the generator can self-repair. | |
| """ | |
| from __future__ import annotations | |
| import importlib | |
| import os | |
| import re | |
| import textwrap | |
| import numpy as np | |
| import trimesh | |
| # LLMs frequently miscall trimesh.creation.revolve (e.g. passing `sections` both positionally | |
| # and as a keyword → "got multiple values for keyword argument 'sections'"). We always want a | |
| # full 360° solid, so wrap it to accept any convention and normalize to revolve(profile, sections=). | |
| def _install_tolerant_revolve(): | |
| real = trimesh.creation.revolve | |
| if getattr(real, "_cf_patched", False): | |
| return | |
| def revolve(*args, **kwargs): | |
| ls = kwargs.pop("linestring", None) | |
| if ls is None and args: | |
| ls, args = args[0], args[1:] | |
| sections = kwargs.get("sections") | |
| if sections is None: | |
| ints = [a for a in args if isinstance(a, int) and a > 3] | |
| sections = ints[-1] if ints else 64 | |
| return real(ls, sections=int(sections)) | |
| revolve._cf_patched = True | |
| trimesh.creation.revolve = revolve | |
| _install_tolerant_revolve() | |
| def _repair_hint(error: str) -> str: | |
| """Targeted guidance for known failure modes so the model doesn't repeat the mistake.""" | |
| e = error.lower() | |
| tips = [] | |
| if "syntaxerror" in e: | |
| tips.append("A comment likely wrapped onto a line without '#'. Remove comments entirely or " | |
| "keep each on ONE line starting with '#'.") | |
| if "revolve" in e: | |
| tips.append("CORRECT revolve usage: `result = trimesh.creation.revolve(profile, sections=64)` " | |
| "where profile = np.array([[r0,h0],[r1,h1],...]) of [radius,height] points. Pass " | |
| "ONLY profile and sections=; do NOT pass angle/cap/any other positional arg.") | |
| if "apply_rotation" in e: | |
| tips.append("There is no apply_rotation; use " | |
| "mesh.apply_transform(trimesh.transformations.rotation_matrix(angle_rad,[x,y,z])).") | |
| if "polygon" in e or "extrude_polygon" in e: | |
| tips.append("extrude_polygon needs a shapely Polygon: " | |
| "from shapely.geometry import Polygon; trimesh.creation.extrude_polygon(Polygon([(x,y),...]), height=H).") | |
| tips.append("If a richer builder keeps failing, FALL BACK to primitives " | |
| "(box/cylinder/sphere/torus) + trimesh.boolean.union/difference — those always work.") | |
| return " ".join(tips) | |
| def _sanitize(code: str) -> str: | |
| """Strip markdown fences and normalize indentation from LLM-emitted code.""" | |
| code = code.strip() | |
| if code.startswith("```"): | |
| code = re.sub(r"^```[a-zA-Z0-9]*\n", "", code) | |
| code = re.sub(r"\n```$", "", code.rstrip()) | |
| # common failure: whole block is uniformly indented -> dedent fixes it | |
| return _autofix_syntax(textwrap.dedent(code).strip() + "\n") | |
| def _autofix_syntax(code: str, max_drops: int = 8) -> str: | |
| """Blank out lines that raise SyntaxError (e.g. a comment that wrapped without '#'). | |
| Such lines are stray prose with no execution value; blanking keeps line numbers stable.""" | |
| lines = code.split("\n") | |
| for _ in range(max_drops): | |
| try: | |
| compile("\n".join(lines), "<gen>", "exec") | |
| break | |
| except SyntaxError as exc: | |
| if exc.lineno and 1 <= exc.lineno <= len(lines) and lines[exc.lineno - 1].strip(): | |
| lines[exc.lineno - 1] = "" | |
| else: | |
| break | |
| return "\n".join(lines) | |
| _ALLOWED_IMPORTS = {"trimesh", "numpy", "math", "shapely"} | |
| def _safe_import(name, globals=None, locals=None, fromlist=(), level=0): | |
| """Restricted __import__: only trimesh / numpy / math (and submodules).""" | |
| root = name.split(".")[0] | |
| if root not in _ALLOWED_IMPORTS: | |
| raise ImportError(f"import of '{name}' is not allowed in the sandbox") | |
| return importlib.import_module(name) | |
| def run_trimesh_code(code: str, out_dir: str, stem: str = "clone") -> tuple[bool, dict]: | |
| """Exec generator code, expecting it to assign `result` (a Trimesh). | |
| Returns (ok, info). On success info has stl_path, glb_path, and mesh stats. | |
| On failure info has {"error": <message>} for self-repair. | |
| """ | |
| # Restricted globals: no builtins beyond a safe minimal set, only trimesh + np. | |
| safe_builtins = { | |
| "range": range, "len": len, "min": min, "max": max, "abs": abs, | |
| "round": round, "float": float, "int": int, "list": list, "dict": dict, | |
| "tuple": tuple, "enumerate": enumerate, "zip": zip, "sum": sum, | |
| "__import__": _safe_import, | |
| } | |
| ns: dict = {"__builtins__": safe_builtins, "trimesh": trimesh, "np": np} | |
| code = _sanitize(code) | |
| try: | |
| exec(code, ns) # noqa: S102 — sandboxed namespace, hackathon scope | |
| except Exception as e: # noqa: BLE001 | |
| return False, {"error": f"{type(e).__name__}: {e}"} | |
| result = ns.get("result") | |
| if not isinstance(result, trimesh.Trimesh): | |
| return False, {"error": "code did not assign a trimesh.Trimesh to `result`"} | |
| if result.is_empty or len(result.vertices) == 0: | |
| return False, {"error": "resulting mesh is empty"} | |
| os.makedirs(out_dir, exist_ok=True) | |
| stl_path = os.path.join(out_dir, f"{stem}.stl") | |
| glb_path = os.path.join(out_dir, f"{stem}.glb") | |
| result.export(stl_path) | |
| result.export(glb_path) | |
| bbox = (result.bounds[1] - result.bounds[0]).tolist() | |
| return True, { | |
| "stl_path": stl_path, | |
| "glb_path": glb_path, | |
| "stats": { | |
| "watertight": bool(result.is_watertight), | |
| "volume_mm3": round(float(result.volume), 1) if result.is_watertight else None, | |
| "bbox_mm": [round(b, 1) for b in bbox], | |
| "n_vertices": len(result.vertices), | |
| "n_faces": len(result.faces), | |
| }, | |
| } | |
| async def make_candidate(plan, spec, generator_fn, out_dir, stem, *, variant_hint=None, max_repairs=1): | |
| """Generate one candidate mesh (with light self-repair). Returns an info dict | |
| (stl_path/glb_path/stats/code/meta) or None. Used for best-of-N parallel generation.""" | |
| feedback = variant_hint | |
| for _ in range(max_repairs + 1): | |
| artifact, meta = await generator_fn(plan, spec, feedback=feedback) | |
| ok, info = run_trimesh_code(artifact.code, out_dir, stem) | |
| if ok: | |
| info["code"], info["meta"] = artifact.code, meta | |
| return info | |
| feedback = (f"Your previous code failed: {info['error']}. {_repair_hint(info['error'])} " | |
| "Return corrected complete code, no fences, no leading indentation, assign `result`.") | |
| return None | |
| async def generate_mesh(plan, spec, generator_fn, out_dir: str, max_repairs: int = 3): | |
| """Generate code -> exec -> on failure feed error back to the generator (<=max_repairs). | |
| Yields (event_text, meta_or_none, info_or_none) tuples for streaming to the UI; | |
| the final yielded info dict (when ok) carries stl/glb paths + stats. | |
| """ | |
| feedback = None | |
| last_code = "" | |
| for attempt in range(max_repairs + 1): | |
| artifact, meta = await generator_fn(plan, spec, feedback=feedback) | |
| last_code = artifact.code | |
| ok, info = run_trimesh_code(artifact.code, out_dir) | |
| if ok: | |
| info["code"] = last_code | |
| yield ("generator", meta, info) | |
| return | |
| # show the model its OWN broken code + the error so it can fix the exact line | |
| feedback = (f"Your previous code:\n```python\n{artifact.code}\n```\n" | |
| f"failed with: {info['error']}\n{_repair_hint(info['error'])}\n" | |
| "Return corrected COMPLETE code. No markdown fences, no leading indentation, " | |
| "assign the final mesh to `result`.") | |
| yield (f"generator (repair {attempt + 1}: {info['error']})", meta, None) | |
| # exhausted repairs | |
| yield ("generator-failed", None, {"error": feedback, "code": last_code}) | |