Spaces:
Sleeping
Sleeping
File size: 8,073 Bytes
8f1f637 fa1e9b4 8f1f637 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | """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})
|