"""Headless Blender render worker — accepts scripts via HTTP, returns rendered images.""" import json import os import random import shutil import string import subprocess import time from http.server import HTTPServer, BaseHTTPRequestHandler GENERATED_DIR = "/data/files/generated" BLENDER_TIMEOUT = 90 # seconds (fits within CF 100s limit) class RenderHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/health": self._json_response(200, {"ok": True, "service": "blender-worker"}) else: self._json_response(404, {"error": "not found"}) def do_POST(self): if self.path != "/render": self._json_response(404, {"error": "not found"}) return try: length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) except (json.JSONDecodeError, ValueError): self._json_response(400, {"error": "invalid JSON"}) return script = body.get("script", "").strip() if not script: self._json_response(400, {"error": "script required"}) return # Write script to temp file script_path = "/tmp/scene.py" render_path = "/tmp/render.png" with open(script_path, "w") as f: f.write(script) # Clean any previous render if os.path.exists(render_path): os.remove(render_path) # Run Blender headless try: result = subprocess.run( ["blender", "--background", "--python", script_path], capture_output=True, text=True, timeout=BLENDER_TIMEOUT, ) except subprocess.TimeoutExpired: self._json_response(504, { "success": False, "error": f"Blender timed out after {BLENDER_TIMEOUT}s", }) return # Check if render was produced if not os.path.exists(render_path): self._json_response(422, { "success": False, "error": "Blender finished but no render output at /tmp/render.png", "stdout": result.stdout[-2000:] if result.stdout else "", "stderr": result.stderr[-2000:] if result.stderr else "", }) return # Move render to shared volume with unique name rand = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) filename = f"blender-{int(time.time())}-{rand}.png" dest = os.path.join(GENERATED_DIR, filename) os.makedirs(GENERATED_DIR, exist_ok=True) shutil.move(render_path, dest) self._json_response(200, { "success": True, "render_url": f"/data/files/generated/{filename}", "filename": filename, }) def _json_response(self, status, data): body = json.dumps(data).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): print(f"[blender-worker] {fmt % args}") if __name__ == "__main__": server = HTTPServer(("0.0.0.0", 8810), RenderHandler) print("[blender-worker] listening on :8810") server.serve_forever()