""" ComfyUI Backend Plugin Plugin adapter for ComfyUI local backend with qwen_image_edit_2509. """ import sys import json import random from pathlib import Path from typing import Any, Dict, Optional, List from PIL import Image # Add parent directories to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from core.comfyui_client import ComfyUIClient from config.settings import Settings # Import from shared plugin system sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'shared')) from plugin_system.base_plugin import BaseBackendPlugin class ComfyUIPlugin(BaseBackendPlugin): """Plugin adapter for ComfyUI backend using qwen_image_edit_2509.""" def __init__(self, config_path: Path): """Initialize ComfyUI plugin.""" super().__init__(config_path) # Get settings settings = Settings() server_address = settings.COMFYUI_BASE_URL.replace("http://", "") try: self.client = ComfyUIClient(server_address=server_address) # Test connection healthy, _ = self.client.health_check() self.available = healthy except Exception as e: print(f"Warning: ComfyUI backend not available: {e}") self.client = None self.available = False # Load qwen workflow template self.workflow_template = None workflow_path = Path(__file__).parent.parent.parent / 'tools' / 'comfyui' / 'workflows' / 'qwen_image_edit.json' if workflow_path.exists(): with open(workflow_path) as f: self.workflow_template = json.load(f) else: print(f"Warning: Workflow template not found at {workflow_path}") def health_check(self) -> bool: """Check if ComfyUI backend is available.""" if not self.available or self.client is None: return False try: healthy, _ = self.client.health_check() return healthy except: return False def _update_qwen_workflow( self, workflow: dict, prompt: str = None, negative_prompt: str = None, input_image_filename: str = None, seed: int = None, width: int = None, height: int = None ) -> dict: """ Update workflow parameters for qwen_image_edit workflow. Node IDs for qwen_image_edit.json: - 111: Positive prompt (TextEncodeQwenImageEditPlus) - 110: Negative prompt (TextEncodeQwenImageEditPlus) - 78: Load Image - 3: KSampler (seed) - 112: EmptySD3LatentImage (width, height) """ # Clone workflow to avoid modifying original wf = json.loads(json.dumps(workflow)) # Update prompt if prompt is not None: wf["111"]["inputs"]["prompt"] = prompt # Update negative prompt if negative_prompt is not None: wf["110"]["inputs"]["prompt"] = negative_prompt # Update input image if input_image_filename is not None: wf["78"]["inputs"]["image"] = input_image_filename # Update seed if seed is not None: wf["3"]["inputs"]["seed"] = seed else: # Random seed if not specified wf["3"]["inputs"]["seed"] = random.randint(1, 2**32 - 1) # Update dimensions if width is not None: wf["112"]["inputs"]["width"] = width if height is not None: wf["112"]["inputs"]["height"] = height return wf def generate_image( self, prompt: str, input_images: Optional[List[Image.Image]] = None, **kwargs ) -> Image.Image: """ Generate image using ComfyUI qwen_image_edit_2509 workflow. Args: prompt: Text prompt for image editing input_images: Optional list of input images (uses first image) **kwargs: Additional parameters (negative_prompt, seed, width, height) Returns: Generated PIL Image """ if not self.health_check(): raise RuntimeError("ComfyUI backend not available") if self.workflow_template is None: raise RuntimeError("Workflow template not loaded") if not input_images or len(input_images) == 0: raise ValueError("qwen_image_edit_2509 requires an input image") # Upload input image input_image = input_images[0] uploaded_filename = self.client.upload_image(input_image) # Get parameters from kwargs negative_prompt = kwargs.get('negative_prompt', '') seed = kwargs.get('seed', None) width = kwargs.get('width', 1024) height = kwargs.get('height', 1024) # Update workflow with parameters workflow = self._update_qwen_workflow( self.workflow_template, prompt=prompt, negative_prompt=negative_prompt, input_image_filename=uploaded_filename, seed=seed, width=width, height=height ) # Execute workflow images = self.client.execute_workflow(workflow) if not images: raise RuntimeError("No images generated") # Return first image return images[0] def get_capabilities(self) -> Dict[str, Any]: """Report ComfyUI backend capabilities.""" return { 'name': 'ComfyUI Local', 'type': 'local', 'supports_input_images': True, 'supports_multi_image': True, 'max_input_images': 16, 'supports_aspect_ratios': True, 'available_aspect_ratios': ['1:1', '3:4', '4:3', '9:16', '16:9'], 'supports_guidance_scale': True, 'supports_inference_steps': True, 'supports_seed': True, 'available_models': [ 'qwen_image_edit_2509', # To be installed 'flux.1_kontext_ai' # To be installed ], 'status': 'partial', # Needs workflow implementation 'estimated_time_per_image': 3.0, # seconds (depends on GPU and model) 'cost_per_image': 0.0, # Free, local }