105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""Headless Blender render worker — accepts scripts via HTTP, returns rendered images."""
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
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)
|
|
os.rename(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()
|