rspace-online/docker/blender-worker/server.py

106 lines
3.4 KiB
Python

"""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()