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:
oib
2025-12-22 10:33:23 +01:00
parent d98b2c7772
commit c8be9d7414
260 changed files with 59033 additions and 351 deletions

View File

@ -0,0 +1,15 @@
"""
Miner plugin system for GPU service execution
"""
from .base import ServicePlugin, PluginResult
from .registry import PluginRegistry
from .exceptions import PluginError, PluginNotFoundError
__all__ = [
"ServicePlugin",
"PluginResult",
"PluginRegistry",
"PluginError",
"PluginNotFoundError"
]

View File

@ -0,0 +1,111 @@
"""
Base plugin interface for GPU service execution
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
from datetime import datetime
import asyncio
@dataclass
class PluginResult:
"""Result from plugin execution"""
success: bool
data: Optional[Dict[str, Any]] = None
error: Optional[str] = None
metrics: Optional[Dict[str, Any]] = None
execution_time: Optional[float] = None
class ServicePlugin(ABC):
"""Base class for all service plugins"""
def __init__(self):
self.service_id = None
self.name = None
self.version = "1.0.0"
self.description = ""
self.capabilities = []
@abstractmethod
async def execute(self, request: Dict[str, Any]) -> PluginResult:
"""Execute the service with given parameters"""
pass
@abstractmethod
def validate_request(self, request: Dict[str, Any]) -> List[str]:
"""Validate request parameters, return list of errors"""
pass
@abstractmethod
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Get hardware requirements for this plugin"""
pass
def get_metrics(self) -> Dict[str, Any]:
"""Get plugin-specific metrics"""
return {
"service_id": self.service_id,
"name": self.name,
"version": self.version
}
async def health_check(self) -> bool:
"""Check if plugin dependencies are available"""
return True
def setup(self) -> None:
"""Initialize plugin resources"""
pass
def cleanup(self) -> None:
"""Cleanup plugin resources"""
pass
class GPUPlugin(ServicePlugin):
"""Base class for GPU-accelerated plugins"""
def __init__(self):
super().__init__()
self.gpu_available = False
self.vram_gb = 0
self.cuda_available = False
def setup(self) -> None:
"""Check GPU availability"""
self._detect_gpu()
def _detect_gpu(self) -> None:
"""Detect GPU and VRAM"""
try:
import torch
if torch.cuda.is_available():
self.gpu_available = True
self.cuda_available = True
self.vram_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
except ImportError:
pass
try:
import GPUtil
gpus = GPUtil.getGPUs()
if gpus:
self.gpu_available = True
self.vram_gb = gpus[0].memoryTotal / 1024
except ImportError:
pass
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Default GPU requirements"""
return {
"gpu": "any",
"vram_gb": 4,
"cuda": "recommended"
}
async def health_check(self) -> bool:
"""Check GPU health"""
return self.gpu_available

View 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

View File

@ -0,0 +1,215 @@
"""
Plugin discovery and matching system
"""
import asyncio
import logging
from typing import Dict, List, Set, Optional
import requests
from .registry import registry
from .base import ServicePlugin
from .exceptions import PluginNotFoundError
logger = logging.getLogger(__name__)
class ServiceDiscovery:
"""Discovers and matches services to plugins"""
def __init__(self, pool_hub_url: str, miner_id: str):
self.pool_hub_url = pool_hub_url
self.miner_id = miner_id
self.enabled_services: Set[str] = set()
self.service_configs: Dict[str, Dict] = {}
self._last_update = 0
self._update_interval = 60 # seconds
async def start(self) -> None:
"""Start the discovery service"""
logger.info("Starting service discovery")
# Initialize plugin registry
await registry.initialize()
# Initial sync
await self.sync_services()
# Start background sync task
asyncio.create_task(self._sync_loop())
async def sync_services(self) -> None:
"""Sync enabled services from pool-hub"""
try:
# Get service configurations from pool-hub
response = requests.get(
f"{self.pool_hub_url}/v1/services/",
headers={"X-Miner-ID": self.miner_id}
)
response.raise_for_status()
services = response.json()
# Update local state
new_enabled = set()
new_configs = {}
for service in services:
if service.get("enabled", False):
service_id = service["service_type"]
new_enabled.add(service_id)
new_configs[service_id] = service
# Find changes
added = new_enabled - self.enabled_services
removed = self.enabled_services - new_enabled
updated = set()
for service_id in self.enabled_services & new_enabled:
if new_configs[service_id] != self.service_configs.get(service_id):
updated.add(service_id)
# Apply changes
for service_id in removed:
await self._disable_service(service_id)
for service_id in added:
await self._enable_service(service_id, new_configs[service_id])
for service_id in updated:
await self._update_service(service_id, new_configs[service_id])
# Update state
self.enabled_services = new_enabled
self.service_configs = new_configs
self._last_update = asyncio.get_event_loop().time()
logger.info(f"Synced services: {len(self.enabled_services)} enabled")
except Exception as e:
logger.error(f"Failed to sync services: {e}")
async def _enable_service(self, service_id: str, config: Dict) -> None:
"""Enable a service"""
try:
# Check if plugin exists
if service_id not in registry.list_plugins():
logger.warning(f"No plugin available for service: {service_id}")
return
# Load plugin
plugin = registry.load_plugin(service_id)
# Validate hardware requirements
await self._validate_hardware_requirements(plugin, config)
# Configure plugin if needed
if hasattr(plugin, 'configure'):
await plugin.configure(config.get('config', {}))
logger.info(f"Enabled service: {service_id}")
except Exception as e:
logger.error(f"Failed to enable service {service_id}: {e}")
async def _disable_service(self, service_id: str) -> None:
"""Disable a service"""
try:
# Unload plugin to free resources
registry.unload_plugin(service_id)
logger.info(f"Disabled service: {service_id}")
except Exception as e:
logger.error(f"Failed to disable service {service_id}: {e}")
async def _update_service(self, service_id: str, config: Dict) -> None:
"""Update service configuration"""
# For now, just disable and re-enable
await self._disable_service(service_id)
await self._enable_service(service_id, config)
async def _validate_hardware_requirements(self, plugin: ServicePlugin, config: Dict) -> None:
"""Validate that miner meets plugin requirements"""
requirements = plugin.get_hardware_requirements()
# This would check against actual miner hardware
# For now, just log the requirements
logger.debug(f"Hardware requirements for {plugin.service_id}: {requirements}")
async def _sync_loop(self) -> None:
"""Background sync loop"""
while True:
await asyncio.sleep(self._update_interval)
await self.sync_services()
async def execute_service(self, service_id: str, request: Dict) -> Dict:
"""Execute a service request"""
try:
# Check if service is enabled
if service_id not in self.enabled_services:
raise PluginNotFoundError(f"Service {service_id} is not enabled")
# Get plugin
plugin = registry.get_plugin(service_id)
if not plugin:
raise PluginNotFoundError(f"No plugin loaded for service: {service_id}")
# Execute request
result = await plugin.execute(request)
# Convert result to dict
return {
"success": result.success,
"data": result.data,
"error": result.error,
"metrics": result.metrics,
"execution_time": result.execution_time
}
except Exception as e:
logger.error(f"Failed to execute service {service_id}: {e}")
return {
"success": False,
"error": str(e)
}
def get_enabled_services(self) -> List[str]:
"""Get list of enabled services"""
return list(self.enabled_services)
def get_service_status(self) -> Dict[str, Dict]:
"""Get status of all services"""
status = {}
for service_id in registry.list_plugins():
plugin = registry.get_plugin(service_id)
status[service_id] = {
"enabled": service_id in self.enabled_services,
"loaded": plugin is not None,
"config": self.service_configs.get(service_id, {}),
"capabilities": plugin.capabilities if plugin else []
}
return status
async def health_check(self) -> Dict[str, bool]:
"""Health check all enabled services"""
results = {}
for service_id in self.enabled_services:
plugin = registry.get_plugin(service_id)
if plugin:
try:
results[service_id] = await plugin.health_check()
except Exception as e:
logger.error(f"Health check failed for {service_id}: {e}")
results[service_id] = False
else:
results[service_id] = False
return results
async def stop(self) -> None:
"""Stop the discovery service"""
logger.info("Stopping service discovery")
registry.cleanup_all()

View File

@ -0,0 +1,23 @@
"""
Plugin system exceptions
"""
class PluginError(Exception):
"""Base exception for plugin errors"""
pass
class PluginNotFoundError(PluginError):
"""Raised when a plugin is not found"""
pass
class PluginValidationError(PluginError):
"""Raised when plugin validation fails"""
pass
class PluginExecutionError(PluginError):
"""Raised when plugin execution fails"""
pass

View File

@ -0,0 +1,318 @@
"""
FFmpeg video processing plugin
"""
import asyncio
import os
import subprocess
import tempfile
from typing import Dict, Any, List
import time
from .base import ServicePlugin, PluginResult
from .exceptions import PluginExecutionError
class FFmpegPlugin(ServicePlugin):
"""Plugin for FFmpeg video processing"""
def __init__(self):
super().__init__()
self.service_id = "ffmpeg"
self.name = "FFmpeg Video Processing"
self.version = "1.0.0"
self.description = "Transcode and process video files using FFmpeg"
self.capabilities = ["transcode", "resize", "compress", "convert"]
def setup(self) -> None:
"""Initialize FFmpeg dependencies"""
# Check for ffmpeg installation
try:
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
self.ffmpeg_path = "ffmpeg"
except (subprocess.CalledProcessError, FileNotFoundError):
raise PluginExecutionError("FFmpeg not found. Install FFmpeg for video processing")
# Check for NVIDIA GPU support
try:
result = subprocess.run(
["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True,
text=True,
check=True
)
self.gpu_acceleration = "h264_nvenc" in result.stdout
except subprocess.CalledProcessError:
self.gpu_acceleration = False
def validate_request(self, request: Dict[str, Any]) -> List[str]:
"""Validate FFmpeg request parameters"""
errors = []
# Check required parameters
if "input_url" not in request and "input_file" not in request:
errors.append("Either 'input_url' or 'input_file' must be provided")
# Validate output format
output_format = request.get("output_format", "mp4")
valid_formats = ["mp4", "avi", "mov", "mkv", "webm", "flv"]
if output_format not in valid_formats:
errors.append(f"Invalid output format. Must be one of: {', '.join(valid_formats)}")
# Validate codec
codec = request.get("codec", "h264")
valid_codecs = ["h264", "h265", "vp9", "av1", "mpeg4"]
if codec not in valid_codecs:
errors.append(f"Invalid codec. Must be one of: {', '.join(valid_codecs)}")
# Validate resolution
resolution = request.get("resolution")
if resolution:
valid_resolutions = ["720p", "1080p", "1440p", "4K", "8K"]
if resolution not in valid_resolutions:
errors.append(f"Invalid resolution. Must be one of: {', '.join(valid_resolutions)}")
# Validate bitrate
bitrate = request.get("bitrate")
if bitrate:
if not isinstance(bitrate, str) or not bitrate.endswith(("k", "M")):
errors.append("Bitrate must end with 'k' or 'M' (e.g., '1000k', '5M')")
# Validate frame rate
fps = request.get("fps")
if fps:
if not isinstance(fps, (int, float)) or fps < 1 or fps > 120:
errors.append("FPS must be between 1 and 120")
return errors
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Get hardware requirements for FFmpeg"""
return {
"gpu": "optional",
"vram_gb": 2,
"ram_gb": 8,
"storage_gb": 10
}
async def execute(self, request: Dict[str, Any]) -> PluginResult:
"""Execute FFmpeg processing"""
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
input_source = request.get("input_url") or request.get("input_file")
output_format = request.get("output_format", "mp4")
codec = request.get("codec", "h264")
resolution = request.get("resolution")
bitrate = request.get("bitrate")
fps = request.get("fps")
gpu_acceleration = request.get("gpu_acceleration", self.gpu_acceleration)
# Get input file
input_file = await self._get_input_file(input_source)
# Build FFmpeg command
cmd = self._build_ffmpeg_command(
input_file=input_file,
output_format=output_format,
codec=codec,
resolution=resolution,
bitrate=bitrate,
fps=fps,
gpu_acceleration=gpu_acceleration
)
# Execute FFmpeg
output_file = await self._execute_ffmpeg(cmd)
# Get output file info
output_info = await self._get_video_info(output_file)
# Clean up input file if downloaded
if input_source != request.get("input_file"):
os.unlink(input_file)
execution_time = time.time() - start_time
return PluginResult(
success=True,
data={
"output_file": output_file,
"output_info": output_info,
"parameters": {
"codec": codec,
"resolution": resolution,
"bitrate": bitrate,
"fps": fps,
"gpu_acceleration": gpu_acceleration
}
},
metrics={
"input_size": os.path.getsize(input_file),
"output_size": os.path.getsize(output_file),
"compression_ratio": os.path.getsize(output_file) / os.path.getsize(input_file),
"processing_time": execution_time,
"real_time_factor": output_info.get("duration", 0) / execution_time if execution_time > 0 else 0
},
execution_time=execution_time
)
except Exception as e:
return PluginResult(
success=False,
error=str(e),
execution_time=time.time() - start_time
)
async def _get_input_file(self, source: str) -> str:
"""Get input file from URL or path"""
if source.startswith(("http://", "https://")):
# Download from URL
import requests
response = requests.get(source, stream=True)
response.raise_for_status()
# Save to temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return f.name
else:
# Local file
if not os.path.exists(source):
raise PluginExecutionError(f"Input file not found: {source}")
return source
def _build_ffmpeg_command(
self,
input_file: str,
output_format: str,
codec: str,
resolution: Optional[str],
bitrate: Optional[str],
fps: Optional[float],
gpu_acceleration: bool
) -> List[str]:
"""Build FFmpeg command"""
cmd = [self.ffmpeg_path, "-i", input_file]
# Add codec
if gpu_acceleration and codec == "h264":
cmd.extend(["-c:v", "h264_nvenc"])
cmd.extend(["-preset", "fast"])
elif gpu_acceleration and codec == "h265":
cmd.extend(["-c:v", "hevc_nvenc"])
cmd.extend(["-preset", "fast"])
else:
cmd.extend(["-c:v", codec])
# Add resolution
if resolution:
resolution_map = {
"720p": ("1280", "720"),
"1080p": ("1920", "1080"),
"1440p": ("2560", "1440"),
"4K": ("3840", "2160"),
"8K": ("7680", "4320")
}
width, height = resolution_map.get(resolution, (None, None))
if width and height:
cmd.extend(["-s", f"{width}x{height}"])
# Add bitrate
if bitrate:
cmd.extend(["-b:v", bitrate])
cmd.extend(["-b:a", "128k"]) # Audio bitrate
# Add FPS
if fps:
cmd.extend(["-r", str(fps)])
# Add audio codec
cmd.extend(["-c:a", "aac"])
# Output file
output_file = tempfile.mktemp(suffix=f".{output_format}")
cmd.append(output_file)
return cmd
async def _execute_ffmpeg(self, cmd: List[str]) -> str:
"""Execute FFmpeg 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 "FFmpeg failed"
raise PluginExecutionError(f"FFmpeg error: {error_msg}")
# Output file is the last argument
return cmd[-1]
async def _get_video_info(self, video_file: str) -> Dict[str, Any]:
"""Get video file information"""
cmd = [
"ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
video_file
]
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return {}
import json
probe_data = json.loads(stdout.decode())
# Extract relevant info
video_stream = next(
(s for s in probe_data.get("streams", []) if s.get("codec_type") == "video"),
{}
)
return {
"duration": float(probe_data.get("format", {}).get("duration", 0)),
"size": int(probe_data.get("format", {}).get("size", 0)),
"width": video_stream.get("width"),
"height": video_stream.get("height"),
"fps": eval(video_stream.get("r_frame_rate", "0/1")),
"codec": video_stream.get("codec_name"),
"bitrate": int(probe_data.get("format", {}).get("bit_rate", 0))
}
async def health_check(self) -> bool:
"""Check FFmpeg health"""
try:
result = subprocess.run(
["ffmpeg", "-version"],
capture_output=True,
check=True
)
return True
except subprocess.CalledProcessError:
return False

View File

@ -0,0 +1,321 @@
"""
LLM inference plugin
"""
import asyncio
from typing import Dict, Any, List, Optional
import time
from .base import GPUPlugin, PluginResult
from .exceptions import PluginExecutionError
class LLMPlugin(GPUPlugin):
"""Plugin for Large Language Model inference"""
def __init__(self):
super().__init__()
self.service_id = "llm_inference"
self.name = "LLM Inference"
self.version = "1.0.0"
self.description = "Run inference on large language models"
self.capabilities = ["generate", "stream", "chat"]
self._model_cache = {}
def setup(self) -> None:
"""Initialize LLM dependencies"""
super().setup()
# Check for transformers installation
try:
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
self.transformers = AutoModelForCausalLM
self.AutoTokenizer = AutoTokenizer
self.pipeline = pipeline
except ImportError:
raise PluginExecutionError("Transformers not installed. Install with: pip install transformers accelerate")
# Check for torch
try:
import torch
self.torch = torch
except ImportError:
raise PluginExecutionError("PyTorch not installed. Install with: pip install torch")
def validate_request(self, request: Dict[str, Any]) -> List[str]:
"""Validate LLM request parameters"""
errors = []
# Check required parameters
if "prompt" not in request:
errors.append("'prompt' is required")
# Validate model
model = request.get("model", "llama-7b")
valid_models = [
"llama-7b",
"llama-13b",
"mistral-7b",
"mixtral-8x7b",
"gpt-3.5-turbo",
"gpt-4"
]
if model not in valid_models:
errors.append(f"Invalid model. Must be one of: {', '.join(valid_models)}")
# Validate max_tokens
max_tokens = request.get("max_tokens", 256)
if not isinstance(max_tokens, int) or max_tokens < 1 or max_tokens > 4096:
errors.append("max_tokens must be an integer between 1 and 4096")
# Validate temperature
temperature = request.get("temperature", 0.7)
if not isinstance(temperature, (int, float)) or temperature < 0.0 or temperature > 2.0:
errors.append("temperature must be between 0.0 and 2.0")
# Validate top_p
top_p = request.get("top_p")
if top_p is not None and (not isinstance(top_p, (int, float)) or top_p <= 0.0 or top_p > 1.0):
errors.append("top_p must be between 0.0 and 1.0")
return errors
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Get hardware requirements for LLM inference"""
return {
"gpu": "recommended",
"vram_gb": 8,
"ram_gb": 16,
"cuda": "recommended"
}
async def execute(self, request: Dict[str, Any]) -> PluginResult:
"""Execute LLM inference"""
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
prompt = request["prompt"]
model_name = request.get("model", "llama-7b")
max_tokens = request.get("max_tokens", 256)
temperature = request.get("temperature", 0.7)
top_p = request.get("top_p", 0.9)
do_sample = request.get("do_sample", True)
stream = request.get("stream", False)
# Load model and tokenizer
model, tokenizer = await self._load_model(model_name)
# Generate response
loop = asyncio.get_event_loop()
if stream:
# Streaming generation
generator = await loop.run_in_executor(
None,
lambda: self._generate_streaming(
model, tokenizer, prompt, max_tokens, temperature, top_p, do_sample
)
)
# Collect all tokens
full_response = ""
tokens = []
for token in generator:
tokens.append(token)
full_response += token
execution_time = time.time() - start_time
return PluginResult(
success=True,
data={
"text": full_response,
"tokens": tokens,
"streamed": True
},
metrics={
"model": model_name,
"prompt_tokens": len(tokenizer.encode(prompt)),
"generated_tokens": len(tokens),
"tokens_per_second": len(tokens) / execution_time if execution_time > 0 else 0
},
execution_time=execution_time
)
else:
# Regular generation
response = await loop.run_in_executor(
None,
lambda: self._generate(
model, tokenizer, prompt, max_tokens, temperature, top_p, do_sample
)
)
execution_time = time.time() - start_time
return PluginResult(
success=True,
data={
"text": response,
"streamed": False
},
metrics={
"model": model_name,
"prompt_tokens": len(tokenizer.encode(prompt)),
"generated_tokens": len(tokenizer.encode(response)) - len(tokenizer.encode(prompt)),
"tokens_per_second": (len(tokenizer.encode(response)) - len(tokenizer.encode(prompt))) / execution_time if execution_time > 0 else 0
},
execution_time=execution_time
)
except Exception as e:
return PluginResult(
success=False,
error=str(e),
execution_time=time.time() - start_time
)
async def _load_model(self, model_name: str):
"""Load LLM model and tokenizer with caching"""
if model_name not in self._model_cache:
loop = asyncio.get_event_loop()
# Map model names to HuggingFace model IDs
model_map = {
"llama-7b": "meta-llama/Llama-2-7b-chat-hf",
"llama-13b": "meta-llama/Llama-2-13b-chat-hf",
"mistral-7b": "mistralai/Mistral-7B-Instruct-v0.1",
"mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1",
"gpt-3.5-turbo": "openai-gpt", # Would need OpenAI API
"gpt-4": "openai-gpt-4" # Would need OpenAI API
}
hf_model = model_map.get(model_name, model_name)
# Load tokenizer
tokenizer = await loop.run_in_executor(
None,
lambda: self.AutoTokenizer.from_pretrained(hf_model)
)
# Load model
device = "cuda" if self.torch.cuda.is_available() else "cpu"
model = await loop.run_in_executor(
None,
lambda: self.transformers.from_pretrained(
hf_model,
torch_dtype=self.torch.float16 if device == "cuda" else self.torch.float32,
device_map="auto" if device == "cuda" else None,
load_in_4bit=True if device == "cuda" and self.vram_gb < 16 else False
)
)
self._model_cache[model_name] = (model, tokenizer)
return self._model_cache[model_name]
def _generate(
self,
model,
tokenizer,
prompt: str,
max_tokens: int,
temperature: float,
top_p: float,
do_sample: bool
) -> str:
"""Generate text without streaming"""
inputs = tokenizer(prompt, return_tensors="pt")
if self.torch.cuda.is_available():
inputs = {k: v.cuda() for k, v in inputs.items()}
with self.torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
do_sample=do_sample,
pad_token_id=tokenizer.eos_token_id
)
# Decode only the new tokens
new_tokens = outputs[0][inputs["input_ids"].shape[1]:]
response = tokenizer.decode(new_tokens, skip_special_tokens=True)
return response
def _generate_streaming(
self,
model,
tokenizer,
prompt: str,
max_tokens: int,
temperature: float,
top_p: float,
do_sample: bool
):
"""Generate text with streaming"""
inputs = tokenizer(prompt, return_tensors="pt")
if self.torch.cuda.is_available():
inputs = {k: v.cuda() for k, v in inputs.items()}
# Simple streaming implementation
# In production, you'd use model.generate with streamer
with self.torch.no_grad():
for i in range(max_tokens):
outputs = model.generate(
**inputs,
max_new_tokens=1,
temperature=temperature,
top_p=top_p,
do_sample=do_sample,
pad_token_id=tokenizer.eos_token_id
)
new_token = outputs[0][-1:]
text = tokenizer.decode(new_token, skip_special_tokens=True)
if text == tokenizer.eos_token:
break
yield text
# Update inputs for next iteration
inputs["input_ids"] = self.torch.cat([inputs["input_ids"], new_token], dim=1)
if "attention_mask" in inputs:
inputs["attention_mask"] = self.torch.cat([
inputs["attention_mask"],
self.torch.ones((1, 1), device=inputs["attention_mask"].device)
], dim=1)
async def health_check(self) -> bool:
"""Check LLM health"""
try:
# Try to load a small model
await self._load_model("mistral-7b")
return True
except Exception:
return False
def cleanup(self) -> None:
"""Cleanup resources"""
# Move models to CPU and clear cache
for model, _ in self._model_cache.values():
if hasattr(model, 'to'):
model.to("cpu")
self._model_cache.clear()
# Clear GPU cache
if self.torch.cuda.is_available():
self.torch.cuda.empty_cache()

View File

@ -0,0 +1,138 @@
"""
Plugin registry for managing service plugins
"""
from typing import Dict, List, Type, Optional
import importlib
import inspect
import logging
from pathlib import Path
from .base import ServicePlugin
from .exceptions import PluginError, PluginNotFoundError
logger = logging.getLogger(__name__)
class PluginRegistry:
"""Registry for managing service plugins"""
def __init__(self):
self._plugins: Dict[str, ServicePlugin] = {}
self._plugin_classes: Dict[str, Type[ServicePlugin]] = {}
self._loaded = False
def register(self, plugin_class: Type[ServicePlugin]) -> None:
"""Register a plugin class"""
plugin_id = getattr(plugin_class, "service_id", plugin_class.__name__)
self._plugin_classes[plugin_id] = plugin_class
logger.info(f"Registered plugin class: {plugin_id}")
def load_plugin(self, service_id: str) -> ServicePlugin:
"""Load and instantiate a plugin"""
if service_id not in self._plugin_classes:
raise PluginNotFoundError(f"Plugin {service_id} not found")
if service_id in self._plugins:
return self._plugins[service_id]
try:
plugin_class = self._plugin_classes[service_id]
plugin = plugin_class()
plugin.setup()
self._plugins[service_id] = plugin
logger.info(f"Loaded plugin: {service_id}")
return plugin
except Exception as e:
logger.error(f"Failed to load plugin {service_id}: {e}")
raise PluginError(f"Failed to load plugin {service_id}: {e}")
def get_plugin(self, service_id: str) -> Optional[ServicePlugin]:
"""Get loaded plugin"""
return self._plugins.get(service_id)
def unload_plugin(self, service_id: str) -> None:
"""Unload a plugin"""
if service_id in self._plugins:
plugin = self._plugins[service_id]
plugin.cleanup()
del self._plugins[service_id]
logger.info(f"Unloaded plugin: {service_id}")
def list_plugins(self) -> List[str]:
"""List all registered plugin IDs"""
return list(self._plugin_classes.keys())
def list_loaded_plugins(self) -> List[str]:
"""List all loaded plugin IDs"""
return list(self._plugins.keys())
async def load_all_from_directory(self, plugin_dir: Path) -> None:
"""Load all plugins from a directory"""
if not plugin_dir.exists():
logger.warning(f"Plugin directory does not exist: {plugin_dir}")
return
for plugin_file in plugin_dir.glob("*.py"):
if plugin_file.name.startswith("_"):
continue
module_name = plugin_file.stem
try:
# Import the module
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find plugin classes in the module
for name, obj in inspect.getmembers(module, inspect.isclass):
if (issubclass(obj, ServicePlugin) and
obj != ServicePlugin and
not name.startswith("_")):
self.register(obj)
logger.info(f"Auto-registered plugin from {module_name}: {name}")
except Exception as e:
logger.error(f"Failed to load plugin from {plugin_file}: {e}")
async def initialize(self, plugin_dir: Optional[Path] = None) -> None:
"""Initialize the plugin registry"""
if self._loaded:
return
# Load built-in plugins
from . import whisper, stable_diffusion, llm_inference, ffmpeg, blender
self.register(whisper.WhisperPlugin)
self.register(stable_diffusion.StableDiffusionPlugin)
self.register(llm_inference.LLMPlugin)
self.register(ffmpeg.FFmpegPlugin)
self.register(blender.BlenderPlugin)
# Load external plugins if directory provided
if plugin_dir:
await self.load_all_from_directory(plugin_dir)
self._loaded = True
logger.info(f"Plugin registry initialized with {len(self._plugin_classes)} plugins")
async def health_check_all(self) -> Dict[str, bool]:
"""Health check all loaded plugins"""
results = {}
for service_id, plugin in self._plugins.items():
try:
results[service_id] = await plugin.health_check()
except Exception as e:
logger.error(f"Health check failed for {service_id}: {e}")
results[service_id] = False
return results
def cleanup_all(self) -> None:
"""Cleanup all loaded plugins"""
for service_id in list(self._plugins.keys()):
self.unload_plugin(service_id)
logger.info("All plugins cleaned up")
# Global registry instance
registry = PluginRegistry()

View File

@ -0,0 +1,281 @@
"""
Stable Diffusion image generation plugin
"""
import asyncio
import base64
import io
from typing import Dict, Any, List
import time
import numpy as np
from .base import GPUPlugin, PluginResult
from .exceptions import PluginExecutionError
class StableDiffusionPlugin(GPUPlugin):
"""Plugin for Stable Diffusion image generation"""
def __init__(self):
super().__init__()
self.service_id = "stable_diffusion"
self.name = "Stable Diffusion"
self.version = "1.0.0"
self.description = "Generate images from text prompts using Stable Diffusion"
self.capabilities = ["txt2img", "img2img"]
self._model_cache = {}
def setup(self) -> None:
"""Initialize Stable Diffusion dependencies"""
super().setup()
# Check for diffusers installation
try:
from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline
self.diffusers = StableDiffusionPipeline
self.img2img_pipe = StableDiffusionImg2ImgPipeline
except ImportError:
raise PluginExecutionError("Diffusers not installed. Install with: pip install diffusers transformers accelerate")
# Check for torch
try:
import torch
self.torch = torch
except ImportError:
raise PluginExecutionError("PyTorch not installed. Install with: pip install torch")
# Check for PIL
try:
from PIL import Image
self.Image = Image
except ImportError:
raise PluginExecutionError("PIL not installed. Install with: pip install Pillow")
def validate_request(self, request: Dict[str, Any]) -> List[str]:
"""Validate Stable Diffusion request parameters"""
errors = []
# Check required parameters
if "prompt" not in request:
errors.append("'prompt' is required")
# Validate model
model = request.get("model", "runwayml/stable-diffusion-v1-5")
valid_models = [
"runwayml/stable-diffusion-v1-5",
"stabilityai/stable-diffusion-2-1",
"stabilityai/stable-diffusion-xl-base-1.0"
]
if model not in valid_models:
errors.append(f"Invalid model. Must be one of: {', '.join(valid_models)}")
# Validate dimensions
width = request.get("width", 512)
height = request.get("height", 512)
if not isinstance(width, int) or width < 256 or width > 1024:
errors.append("Width must be an integer between 256 and 1024")
if not isinstance(height, int) or height < 256 or height > 1024:
errors.append("Height must be an integer between 256 and 1024")
# Validate steps
steps = request.get("steps", 20)
if not isinstance(steps, int) or steps < 1 or steps > 100:
errors.append("Steps must be an integer between 1 and 100")
# Validate guidance scale
guidance_scale = request.get("guidance_scale", 7.5)
if not isinstance(guidance_scale, (int, float)) or guidance_scale < 1.0 or guidance_scale > 20.0:
errors.append("Guidance scale must be between 1.0 and 20.0")
# Check img2img requirements
if request.get("task") == "img2img":
if "init_image" not in request:
errors.append("'init_image' is required for img2img task")
strength = request.get("strength", 0.8)
if not isinstance(strength, (int, float)) or strength < 0.0 or strength > 1.0:
errors.append("Strength must be between 0.0 and 1.0")
return errors
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Get hardware requirements for Stable Diffusion"""
return {
"gpu": "required",
"vram_gb": 6,
"ram_gb": 8,
"cuda": "required"
}
async def execute(self, request: Dict[str, Any]) -> PluginResult:
"""Execute Stable Diffusion generation"""
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
prompt = request["prompt"]
negative_prompt = request.get("negative_prompt", "")
model_name = request.get("model", "runwayml/stable-diffusion-v1-5")
width = request.get("width", 512)
height = request.get("height", 512)
steps = request.get("steps", 20)
guidance_scale = request.get("guidance_scale", 7.5)
num_images = request.get("num_images", 1)
seed = request.get("seed")
task = request.get("task", "txt2img")
# Load model
pipe = await self._load_model(model_name)
# Generate images
loop = asyncio.get_event_loop()
if task == "img2img":
# Handle img2img
init_image_data = request["init_image"]
init_image = self._decode_image(init_image_data)
strength = request.get("strength", 0.8)
images = await loop.run_in_executor(
None,
lambda: pipe(
prompt=prompt,
negative_prompt=negative_prompt,
image=init_image,
strength=strength,
num_inference_steps=steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_images,
generator=self._get_generator(seed)
).images
)
else:
# Handle txt2img
images = await loop.run_in_executor(
None,
lambda: pipe(
prompt=prompt,
negative_prompt=negative_prompt,
width=width,
height=height,
num_inference_steps=steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_images,
generator=self._get_generator(seed)
).images
)
# Encode images to base64
encoded_images = []
for img in images:
buffer = io.BytesIO()
img.save(buffer, format="PNG")
encoded_images.append(base64.b64encode(buffer.getvalue()).decode())
execution_time = time.time() - start_time
return PluginResult(
success=True,
data={
"images": encoded_images,
"count": len(images),
"parameters": {
"prompt": prompt,
"width": width,
"height": height,
"steps": steps,
"guidance_scale": guidance_scale,
"seed": seed
}
},
metrics={
"model": model_name,
"task": task,
"images_generated": len(images),
"generation_time": execution_time,
"time_per_image": execution_time / len(images)
},
execution_time=execution_time
)
except Exception as e:
return PluginResult(
success=False,
error=str(e),
execution_time=time.time() - start_time
)
async def _load_model(self, model_name: str):
"""Load Stable Diffusion model with caching"""
if model_name not in self._model_cache:
loop = asyncio.get_event_loop()
# Determine device
device = "cuda" if self.torch.cuda.is_available() else "cpu"
# Load with attention slicing for memory efficiency
pipe = await loop.run_in_executor(
None,
lambda: self.diffusers.from_pretrained(
model_name,
torch_dtype=self.torch.float16 if device == "cuda" else self.torch.float32,
safety_checker=None,
requires_safety_checker=False
)
)
pipe = pipe.to(device)
# Enable memory optimizations
if device == "cuda":
pipe.enable_attention_slicing()
if self.vram_gb < 8:
pipe.enable_model_cpu_offload()
self._model_cache[model_name] = pipe
return self._model_cache[model_name]
def _decode_image(self, image_data: str) -> 'Image':
"""Decode base64 image"""
if image_data.startswith('data:image'):
# Remove data URL prefix
image_data = image_data.split(',')[1]
image_bytes = base64.b64decode(image_data)
return self.Image.open(io.BytesIO(image_bytes))
def _get_generator(self, seed: Optional[int]):
"""Get torch generator for reproducible results"""
if seed is not None:
return self.torch.Generator().manual_seed(seed)
return None
async def health_check(self) -> bool:
"""Check Stable Diffusion health"""
try:
# Try to load a small model
pipe = await self._load_model("runwayml/stable-diffusion-v1-5")
return pipe is not None
except Exception:
return False
def cleanup(self) -> None:
"""Cleanup resources"""
# Move models to CPU and clear cache
for pipe in self._model_cache.values():
if hasattr(pipe, 'to'):
pipe.to("cpu")
self._model_cache.clear()
# Clear GPU cache
if self.torch.cuda.is_available():
self.torch.cuda.empty_cache()

View File

@ -0,0 +1,215 @@
"""
Whisper speech recognition plugin
"""
import asyncio
import os
import tempfile
from typing import Dict, Any, List
import time
from .base import GPUPlugin, PluginResult
from .exceptions import PluginExecutionError
class WhisperPlugin(GPUPlugin):
"""Plugin for Whisper speech recognition"""
def __init__(self):
super().__init__()
self.service_id = "whisper"
self.name = "Whisper Speech Recognition"
self.version = "1.0.0"
self.description = "Transcribe and translate audio files using OpenAI Whisper"
self.capabilities = ["transcribe", "translate"]
self._model_cache = {}
def setup(self) -> None:
"""Initialize Whisper dependencies"""
super().setup()
# Check for whisper installation
try:
import whisper
self.whisper = whisper
except ImportError:
raise PluginExecutionError("Whisper not installed. Install with: pip install openai-whisper")
# Check for ffmpeg
import subprocess
try:
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
raise PluginExecutionError("FFmpeg not found. Install FFmpeg for audio processing")
def validate_request(self, request: Dict[str, Any]) -> List[str]:
"""Validate Whisper request parameters"""
errors = []
# Check required parameters
if "audio_url" not in request and "audio_file" not in request:
errors.append("Either 'audio_url' or 'audio_file' must be provided")
# Validate model
model = request.get("model", "base")
valid_models = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3"]
if model not in valid_models:
errors.append(f"Invalid model. Must be one of: {', '.join(valid_models)}")
# Validate task
task = request.get("task", "transcribe")
if task not in ["transcribe", "translate"]:
errors.append("Task must be 'transcribe' or 'translate'")
# Validate language
if "language" in request:
language = request["language"]
if not isinstance(language, str) or len(language) != 2:
errors.append("Language must be a 2-letter language code (e.g., 'en', 'es')")
return errors
def get_hardware_requirements(self) -> Dict[str, Any]:
"""Get hardware requirements for Whisper"""
return {
"gpu": "recommended",
"vram_gb": 2,
"ram_gb": 4,
"storage_gb": 1
}
async def execute(self, request: Dict[str, Any]) -> PluginResult:
"""Execute Whisper transcription"""
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
model_name = request.get("model", "base")
task = request.get("task", "transcribe")
language = request.get("language")
temperature = request.get("temperature", 0.0)
# Load or get cached model
model = await self._load_model(model_name)
# Get audio file
audio_path = await self._get_audio_file(request)
# Transcribe
loop = asyncio.get_event_loop()
if task == "translate":
result = await loop.run_in_executor(
None,
lambda: model.transcribe(
audio_path,
task="translate",
temperature=temperature
)
)
else:
result = await loop.run_in_executor(
None,
lambda: model.transcribe(
audio_path,
language=language,
temperature=temperature
)
)
# Clean up
if audio_path != request.get("audio_file"):
os.unlink(audio_path)
execution_time = time.time() - start_time
return PluginResult(
success=True,
data={
"text": result["text"],
"language": result.get("language"),
"segments": result.get("segments", [])
},
metrics={
"model": model_name,
"task": task,
"audio_duration": result.get("duration"),
"processing_time": execution_time,
"real_time_factor": result.get("duration", 0) / execution_time if execution_time > 0 else 0
},
execution_time=execution_time
)
except Exception as e:
return PluginResult(
success=False,
error=str(e),
execution_time=time.time() - start_time
)
async def _load_model(self, model_name: str):
"""Load Whisper model with caching"""
if model_name not in self._model_cache:
loop = asyncio.get_event_loop()
model = await loop.run_in_executor(
None,
lambda: self.whisper.load_model(model_name)
)
self._model_cache[model_name] = model
return self._model_cache[model_name]
async def _get_audio_file(self, request: Dict[str, Any]) -> str:
"""Get audio file from URL or direct file path"""
if "audio_file" in request:
return request["audio_file"]
# Download from URL
audio_url = request["audio_url"]
# Use requests to download
import requests
response = requests.get(audio_url, stream=True)
response.raise_for_status()
# Save to temporary file
suffix = self._get_audio_suffix(audio_url)
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return f.name
def _get_audio_suffix(self, url: str) -> str:
"""Get file extension from URL"""
if url.endswith('.mp3'):
return '.mp3'
elif url.endswith('.wav'):
return '.wav'
elif url.endswith('.m4a'):
return '.m4a'
elif url.endswith('.flac'):
return '.flac'
else:
return '.mp3' # Default
async def health_check(self) -> bool:
"""Check Whisper health"""
try:
# Check if we can load the tiny model
await self._load_model("tiny")
return True
except Exception:
return False
def cleanup(self) -> None:
"""Cleanup resources"""
self._model_cache.clear()