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
This commit is contained in:
371
apps/miner-node/plugins/blender.py
Normal file
371
apps/miner-node/plugins/blender.py
Normal file
@ -0,0 +1,371 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user