11 KiB
11 KiB
Plugin Development Quick Reference
Quick Start
1. Create Plugin File
# my_plugin.py
from mjr_am_backend.features.metadata.plugin_system.base import (
MetadataExtractorPlugin,
ExtractionResult,
)
class MyExtractor(MetadataExtractorPlugin):
@property
def name(self): return "my_extractor"
@property
def supported_extensions(self): return ['.png']
@property
def priority(self): return 50
async def extract(self, filepath):
data = {"key": "value"}
return self._create_success_result(data)
2. Install Plugin
# Copy to plugin directory
cp my_plugin.py ~/.comfyui/majoor_plugins/extractors/
3. Reload Plugins
# Via API
curl -X POST http://localhost:8188/mjr/am/plugins/reload
# Or restart ComfyUI
Plugin API
Required Properties
| Property | Type | Description |
|---|---|---|
name |
str | Unique identifier (lowercase, underscores) |
supported_extensions |
list[str] | File extensions (e.g., ['.png', '.webp']) |
priority |
int | Higher = runs first (100-999 custom, 50-99 format, 1-49 generic) |
Required Methods
| Method | Signature | Description |
|---|---|---|
extract |
async def extract(self, filepath: str) -> ExtractionResult |
Main extraction logic |
Optional Properties
| Property | Type | Default | Description |
|---|---|---|---|
metadata |
ExtractorMetadata |
Auto | Plugin info (version, author, description) |
min_compatibility_version |
str | "2.4.9" | Minimum Majoor version |
Optional Methods
| Method | Signature | Description |
|---|---|---|
can_extract |
def can_extract(self, filepath: str) -> bool |
Check if can handle file |
pre_extract |
async def pre_extract(self, filepath: str) -> bool |
Pre-extraction validation |
post_extract |
async def post_extract(self, filepath, result) |
Post-extraction enrichment |
cleanup |
async def cleanup(self) |
Cleanup on unload |
Helper Methods
| Method | Signature | Description |
|---|---|---|
_create_success_result |
def _create_success_result(self, data, confidence=1.0) |
Create success result |
_create_error_result |
def _create_error_result(self, error) |
Create error result |
ExtractionResult
@dataclass
class ExtractionResult:
success: bool # True if extraction succeeded
data: Dict[str, Any] # Extracted metadata
error: Optional[str] # Error message if failed
extractor_name: str # Plugin name
confidence: float # 0.0-1.0 confidence score
Standard Metadata Fields
{
# Standard ComfyUI metadata
"prompt": str, # Main prompt
"negative_prompt": str, # Negative prompt
"seed": int, # Random seed
"steps": int, # Sampling steps
"sampler": str, # Sampler name
"cfg": float, # CFG scale
"models": list, # Model names
"loras": list, # LoRA info
# Custom data
"custom_data": dict, # Node-specific data
"workflow": dict, # Workflow JSON
"file_info": dict, # File information
}
Plugin Lifecycle
┌─────────────────────────────────────────────────────────────┐
│ 1. Discovery │
│ - Plugin file found in plugin directory │
│ - Validated by PluginValidator │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Loading │
│ - Module imported │
│ - Extractor classes instantiated │
│ - Registered in PluginLoader │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Runtime │
│ - can_extract() called to check compatibility │
│ - pre_extract() called for validation │
│ - extract() called for extraction │
│ - post_extract() called for enrichment │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Cleanup │
│ - cleanup() called on unload │
│ - Resources released │
└─────────────────────────────────────────────────────────────┘
Security
Blocked Patterns
The following patterns are blocked during validation:
# Code execution
eval()
exec()
compile()
# OS access
os.system()
os.popen()
subprocess.*
# Network access
socket.*
requests.*
urllib.*
# Unsafe deserialization
pickle.load()
marshal.load()
# Dynamic imports
__import__()
importlib.reload()
Allowed Imports
# Standard library (safe)
typing, collections, functools, itertools, pathlib
json, re, hashlib, base64, io, struct
logging, dataclasses, enum, abc, contextlib
asyncio, datetime, time, math
# Third-party (safe)
PIL (Pillow)
numpy
Testing
Unit Test Example
import pytest
from my_plugin import MyExtractor
@pytest.mark.asyncio
async def test_extraction(tmp_path):
extractor = MyExtractor()
# Create test file
test_file = tmp_path / "test.png"
test_file.write_bytes(b"fake png data")
# Test extraction
result = await extractor.extract(str(test_file))
assert result.success is True
assert "key" in result.data
assert result.confidence > 0
Debug Logging
# Enable debug logging
export MJR_PLUGIN_LOG_LEVEL=DEBUG
# In plugin code
logger.debug(f"Debug message: {data}")
logger.info(f"Info message: {data}")
logger.warning(f"Warning message: {data}")
logger.error(f"Error message: {error}")
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/mjr/am/plugins/list |
GET | List all plugins |
/mjr/am/plugins/{name}/enable |
POST | Enable plugin |
/mjr/am/plugins/{name}/disable |
POST | Disable plugin |
/mjr/am/plugins/reload |
POST | Reload all plugins |
Example Requests
# List plugins
curl http://localhost:8188/mjr/am/plugins/list
# Enable plugin
curl -X POST http://localhost:8188/mjr/am/plugins/my_extractor/enable
# Disable plugin
curl -X POST http://localhost:8188/mjr/am/plugins/my_extractor/disable
# Reload plugins
curl -X POST http://localhost:8188/mjr/am/plugins/reload
Troubleshooting
Plugin Not Loading
| Symptom | Solution |
|---|---|
| No error in logs | Check file is in correct directory |
| Syntax error | Fix Python syntax in plugin file |
| Validation failed | Remove blocked patterns |
| Import error | Check import paths are valid |
Extraction Fails
| Symptom | Solution |
|---|---|
| File not found | Verify filepath is absolute |
| Permission denied | Check file permissions |
| Timeout | Optimize extraction logic |
| Low confidence | Improve data extraction |
Performance Issues
| Symptom | Solution |
|---|---|
| Slow extraction | Add caching, optimize logic |
| High memory | Release resources in cleanup() |
| Blocking I/O | Use async operations |
Best Practices
Code Organization
# Good: Organized extraction logic
class MyExtractor(MetadataExtractorPlugin):
async def extract(self, filepath):
if filepath.endswith('.png'):
return await self._extract_png(filepath)
elif filepath.endswith('.json'):
return await self._extract_json(filepath)
return self._create_error_result("Unsupported format")
async def _extract_png(self, filepath):
# PNG-specific logic
pass
async def _extract_json(self, filepath):
# JSON-specific logic
pass
Error Handling
# Good: Comprehensive error handling
async def extract(self, filepath):
try:
# Pre-checks
if not Path(filepath).exists():
return self._create_error_result("File not found")
# Extraction
data = await self._do_extract(filepath)
return self._create_success_result(data)
except FileNotFoundError as e:
return self._create_error_result(f"File not found: {e}")
except PermissionError as e:
return self._create_error_result(f"Permission denied: {e}")
except Exception as e:
logger.exception(f"Extraction failed")
return self._create_error_result(str(e))
Logging
# Good: Appropriate logging levels
logger.debug(f"Processing file: {filepath}") # Debug info
logger.info(f"Extracted metadata from {filepath}") # Success
logger.warning(f"Missing field: {field}") # Non-critical issue
logger.error(f"Extraction failed: {error}") # Critical error
Examples
See plugins/examples/ for complete examples:
wanvideo_extractor.py- WanVideo metadata extractioncustom_node_extractor.py- Template with documentation
Resources
docs/PLUGIN_SYSTEM_DESIGN.md- Full architecturemjr_am_backend/features/metadata/plugin_system/base.py- API referenceplugins/README.md- Installation guide