cadforge / server /executor.py
eventhorizon28's picture
Upload folder using huggingface_hub
7c72eb2 verified
import json
import logging
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
logger = logging.getLogger(__name__)
RUNNER_SCRIPT = '''
import sys
import json
import traceback
try:
import cadquery as cq
code = sys.stdin.read()
local_ns = {"cq": cq, "cadquery": cq}
try:
import math
local_ns["math"] = math
except Exception:
pass
exec(code, local_ns)
if "result" not in local_ns:
print(json.dumps({"success": False, "error": "No variable named 'result' was defined"}))
sys.exit(0)
result = local_ns["result"]
if hasattr(result, "val"):
shape = result.val()
else:
shape = result
bb = shape.BoundingBox()
volume = shape.Volume()
is_valid = shape.isValid()
faces = shape.Faces()
face_types = [f.geomType() for f in faces]
from collections import Counter
type_counts = dict(Counter(face_types))
total_faces = len(face_types)
dominant = max(type_counts, key=type_counts.get) if type_counts else "UNKNOWN"
edges = shape.Edges()
vertices = shape.Vertices()
dims = [bb.xlen, bb.ylen, bb.zlen]
sorted_axes = sorted(zip(["X", "Y", "Z"], dims), key=lambda x: x[1], reverse=True)
longest_axis = sorted_axes[0][0]
max_dim = max(dims) if max(dims) > 0 else 1.0
shells = shape.Shells()
is_watertight = True
if not shells:
is_watertight = False
else:
for s in shells:
if not s.Closed():
is_watertight = False
break
V = len(vertices)
E = len(edges)
F = total_faces
output = {
"success": True,
"properties": {
"is_valid": is_valid,
"is_watertight": is_watertight,
"volume_mm3": round(volume, 4),
"surface_area_mm2": round(shape.Area(), 4),
"bbox_x_mm": round(bb.xlen, 4),
"bbox_y_mm": round(bb.ylen, 4),
"bbox_z_mm": round(bb.zlen, 4),
"bbox_longest_axis": longest_axis,
"bbox_ratio_yx": round(sorted_axes[1][1] / max_dim, 4),
"bbox_ratio_zx": round(sorted_axes[2][1] / max_dim, 4),
"face_count": total_faces,
"face_type_counts": type_counts,
"dominant_face_type": dominant,
"face_type_distribution": {k: round(v / total_faces, 4) for k, v in type_counts.items()} if total_faces > 0 else {},
"edge_count": E,
"vertex_count": V,
"euler_characteristic": V - E + F,
}
}
print(json.dumps(output))
except Exception as e:
tb = traceback.format_exc()
print(json.dumps({"success": False, "error": str(e), "traceback": tb}))
'''
def execute_cadquery_code(
code: str,
timeout: float = 10.0,
python_path: Optional[str] = None,
) -> Dict[str, Any]:
t0 = time.time()
if python_path is None:
python_path = sys.executable
logger.info(f"Executing CadQuery code ({len(code)} chars, timeout={timeout}s)")
try:
proc = subprocess.run(
[python_path, "-c", RUNNER_SCRIPT],
input=code,
capture_output=True,
text=True,
timeout=timeout,
)
elapsed = time.time() - t0
logger.info(f"Subprocess completed in {elapsed:.3f}s, returncode={proc.returncode}")
stdout = proc.stdout.strip()
stderr = proc.stderr.strip()
if proc.returncode != 0 and not stdout:
return {
"success": False,
"error": stderr or f"Process exited with code {proc.returncode}",
"properties": None,
}
if not stdout:
return {
"success": False,
"error": "No output from subprocess",
"properties": None,
}
result = json.loads(stdout)
if not result.get("success", False):
return {
"success": False,
"error": result.get("error", "Unknown error"),
"properties": None,
}
return {
"success": True,
"error": None,
"properties": result["properties"],
}
except subprocess.TimeoutExpired:
elapsed = time.time() - t0
logger.warning(f"CadQuery execution timed out after {elapsed:.3f}s")
return {
"success": False,
"error": f"Code execution timed out after {timeout} seconds",
"properties": None,
}
except json.JSONDecodeError as e:
elapsed = time.time() - t0
logger.error(f"Failed to parse subprocess output after {elapsed:.3f}s: {e}")
return {
"success": False,
"error": f"Failed to parse execution output: {e}",
"properties": None,
}
except Exception as e:
elapsed = time.time() - t0
logger.error(f"execute_cadquery_code failed after {elapsed:.3f}s: {e}")
return {
"success": False,
"error": str(e),
"properties": None,
}