Files
oib c8be9d7414 feat: add marketplace metrics, privacy features, and service registry endpoints
- Add Prometheus metrics for marketplace API throughput and error rates with new dashboard panels
- Implement confidential transaction models with encryption support and access control
- Add key management system with registration, rotation, and audit logging
- Create services and registry routers for service discovery and management
- Integrate ZK proof generation for privacy-preserving receipts
- Add metrics instru
2025-12-22 10:33:23 +01:00

372 lines
13 KiB
Python

"""
Blender 3D rendering plugin
"""
import asyncio
import os
import subprocess
import tempfile
import json
from typing import Dict, Any, List, Optional
import time
from .base import GPUPlugin, PluginResult
from .exceptions import PluginExecutionError
class BlenderPlugin(GPUPlugin):
"""Plugin for Blender 3D rendering"""
def __init__(self):
super().__init__()
self.service_id = "blender"
self.name = "Blender Rendering"
self.version = "1.0.0"
self.description = "Render 3D scenes using Blender"
self.capabilities = ["render", "animation", "cycles", "eevee"]
def setup(self) -> None:
"""Initialize Blender dependencies"""
super().setup()
# Check for Blender installation
try:
result = subprocess.run(
["blender", "--version"],
capture_output=True,
text=True,
check=True
)
self.blender_path = "blender"
except (subprocess.CalledProcessError, FileNotFoundError):
raise PluginExecutionError("Blender not found. Install Blender for 3D rendering")
# Check for bpy module (Python API)
try:
import bpy
self.bpy_available = True
except ImportError:
self.bpy_available = False
print("Warning: bpy module not available. Some features may be limited.")
def validate_request(self, request: Dict[str, Any]) -> List[str]:
"""Validate Blender request parameters"""
errors = []
# Check required parameters
if "blend_file" not in request and "scene_data" not in request:
errors.append("Either 'blend_file' or 'scene_data' must be provided")
# Validate engine
engine = request.get("engine", "cycles")
valid_engines = ["cycles", "eevee", "workbench"]
if engine not in valid_engines:
errors.append(f"Invalid engine. Must be one of: {', '.join(valid_engines)}")
# Validate resolution
resolution_x = request.get("resolution_x", 1920)
resolution_y = request.get("resolution_y", 1080)
if not isinstance(resolution_x, int) or resolution_x < 1 or resolution_x > 65536:
errors.append("resolution_x must be an integer between 1 and 65536")
if not isinstance(resolution_y, int) or resolution_y < 1 or resolution_y > 65536:
errors.append("resolution_y must be an integer between 1 and 65536")
# Validate samples
samples = request.get("samples", 128)
if not isinstance(samples, int) or samples < 1 or samples > 10000:
errors.append("samples must be an integer between 1 and 10000")
# Validate frame range for animation
if request.get("animation", False):
frame_start = request.get("frame_start", 1)
frame_end = request.get("frame_end", 250)
if not isinstance(frame_start, int) or frame_start < 1:
errors.append("frame_start must be >= 1")
if not isinstance(frame_end, int) or frame_end < frame_start:
errors.append("frame_end must be >= frame_start")
return errors
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Get hardware requirements for Blender"""
return {
"gpu": "recommended",
"vram_gb": 4,
"ram_gb": 16,
"cuda": "recommended"
}
async def execute(self, request: Dict[str, Any]) -> PluginResult:
"""Execute Blender rendering"""
start_time = time.time()
try:
# Validate request
errors = self.validate_request(request)
if errors:
return PluginResult(
success=False,
error=f"Validation failed: {'; '.join(errors)}"
)
# Get parameters
blend_file = request.get("blend_file")
scene_data = request.get("scene_data")
engine = request.get("engine", "cycles")
resolution_x = request.get("resolution_x", 1920)
resolution_y = request.get("resolution_y", 1080)
samples = request.get("samples", 128)
animation = request.get("animation", False)
frame_start = request.get("frame_start", 1)
frame_end = request.get("frame_end", 250)
output_format = request.get("output_format", "png")
gpu_acceleration = request.get("gpu_acceleration", self.gpu_available)
# Prepare input file
input_file = await self._prepare_input_file(blend_file, scene_data)
# Build Blender command
cmd = self._build_blender_command(
input_file=input_file,
engine=engine,
resolution_x=resolution_x,
resolution_y=resolution_y,
samples=samples,
animation=animation,
frame_start=frame_start,
frame_end=frame_end,
output_format=output_format,
gpu_acceleration=gpu_acceleration
)
# Execute Blender
output_files = await self._execute_blender(cmd, animation, frame_start, frame_end)
# Get render statistics
render_stats = await self._get_render_stats(output_files[0] if output_files else None)
# Clean up input file if created from scene data
if scene_data:
os.unlink(input_file)
execution_time = time.time() - start_time
return PluginResult(
success=True,
data={
"output_files": output_files,
"count": len(output_files),
"animation": animation,
"parameters": {
"engine": engine,
"resolution": f"{resolution_x}x{resolution_y}",
"samples": samples,
"gpu_acceleration": gpu_acceleration
}
},
metrics={
"engine": engine,
"frames_rendered": len(output_files),
"render_time": execution_time,
"time_per_frame": execution_time / len(output_files) if output_files else 0,
"samples_per_second": (samples * len(output_files)) / execution_time if execution_time > 0 else 0,
"render_stats": render_stats
},
execution_time=execution_time
)
except Exception as e:
return PluginResult(
success=False,
error=str(e),
execution_time=time.time() - start_time
)
async def _prepare_input_file(self, blend_file: Optional[str], scene_data: Optional[Dict]) -> str:
"""Prepare input .blend file"""
if blend_file:
# Use provided file
if not os.path.exists(blend_file):
raise PluginExecutionError(f"Blend file not found: {blend_file}")
return blend_file
elif scene_data:
# Create blend file from scene data
if not self.bpy_available:
raise PluginExecutionError("Cannot create scene without bpy module")
# Create a temporary Python script to generate the scene
script = tempfile.mktemp(suffix=".py")
output_blend = tempfile.mktemp(suffix=".blend")
with open(script, "w") as f:
f.write(f"""
import bpy
import json
# Load scene data
scene_data = json.loads('''{json.dumps(scene_data)}''')
# Clear default scene
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create scene from data
# This is a simplified example - in practice, you'd parse the scene_data
# and create appropriate objects, materials, lights, etc.
# Save blend file
bpy.ops.wm.save_as_mainfile(filepath='{output_blend}')
""")
# Run Blender to create the scene
cmd = [self.blender_path, "--background", "--python", script]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
# Clean up script
os.unlink(script)
return output_blend
else:
raise PluginExecutionError("Either blend_file or scene_data must be provided")
def _build_blender_command(
self,
input_file: str,
engine: str,
resolution_x: int,
resolution_y: int,
samples: int,
animation: bool,
frame_start: int,
frame_end: int,
output_format: str,
gpu_acceleration: bool
) -> List[str]:
"""Build Blender command"""
cmd = [
self.blender_path,
"--background",
input_file,
"--render-engine", engine,
"--render-format", output_format.upper()
]
# Add Python script for settings
script = tempfile.mktemp(suffix=".py")
with open(script, "w") as f:
f.write(f"""
import bpy
# Set resolution
bpy.context.scene.render.resolution_x = {resolution_x}
bpy.context.scene.render.resolution_y = {resolution_y}
# Set samples for Cycles
if bpy.context.scene.render.engine == 'CYCLES':
bpy.context.scene.cycles.samples = {samples}
# Enable GPU rendering if available
if {str(gpu_acceleration).lower()}:
bpy.context.scene.cycles.device = 'GPU'
preferences = bpy.context.preferences
cycles_preferences = preferences.addons['cycles'].preferences
cycles_preferences.compute_device_type = 'CUDA'
cycles_preferences.get_devices()
for device in cycles_preferences.devices:
device.use = True
# Set frame range for animation
if {str(animation).lower()}:
bpy.context.scene.frame_start = {frame_start}
bpy.context.scene.frame_end = {frame_end}
# Set output path
bpy.context.scene.render.filepath = '{tempfile.mkdtemp()}/render_'
# Save settings
bpy.ops.wm.save_mainfile()
""")
cmd.extend(["--python", script])
# Add render command
if animation:
cmd.extend(["-a"]) # Render animation
else:
cmd.extend(["-f", "1"]) # Render single frame
return cmd
async def _execute_blender(
self,
cmd: List[str],
animation: bool,
frame_start: int,
frame_end: int
) -> List[str]:
"""Execute Blender command"""
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode() if stderr else "Blender failed"
raise PluginExecutionError(f"Blender error: {error_msg}")
# Find output files
output_dir = tempfile.mkdtemp()
output_pattern = os.path.join(output_dir, "render_*")
if animation:
# Animation creates multiple files
import glob
output_files = glob.glob(output_pattern)
output_files.sort() # Ensure frame order
else:
# Single frame
output_files = [glob.glob(output_pattern)[0]]
return output_files
async def _get_render_stats(self, output_file: Optional[str]) -> Dict[str, Any]:
"""Get render statistics"""
if not output_file or not os.path.exists(output_file):
return {}
# Get file size and basic info
file_size = os.path.getsize(output_file)
# Try to get image dimensions
try:
from PIL import Image
with Image.open(output_file) as img:
width, height = img.size
except:
width = height = None
return {
"file_size": file_size,
"width": width,
"height": height,
"format": os.path.splitext(output_file)[1][1:].upper()
}
async def health_check(self) -> bool:
"""Check Blender health"""
try:
result = subprocess.run(
["blender", "--version"],
capture_output=True,
check=True
)
return True
except subprocess.CalledProcessError:
return False