From a9d793749fac49583efdd941ce231c5f3b9ab516 Mon Sep 17 00:00:00 2001 From: puke Date: Sun, 26 Oct 2025 13:12:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E9=80=A0image=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + reelforge/capabilities/image.py | 140 ------- reelforge/service.py | 11 +- reelforge/services/image.py | 364 ++++++++++++++++-- reelforge/services/storyboard_processor.py | 3 +- web.py | 2 +- ..._by_local_flux.json => image_default.json} | 0 7 files changed, 337 insertions(+), 188 deletions(-) delete mode 100644 reelforge/capabilities/image.py rename workflows/{t2i_by_local_flux.json => image_default.json} (100%) diff --git a/.gitignore b/.gitignore index 4031925..749dc95 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,8 @@ output plans/ examples/ repositories/ + +# Workflows - ignore user customizations but keep defaults +workflows/* +!workflows/*_default.json +!workflows/README.md diff --git a/reelforge/capabilities/image.py b/reelforge/capabilities/image.py deleted file mode 100644 index 217fd2d..0000000 --- a/reelforge/capabilities/image.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Image Generation Capabilities using ComfyKit - -ComfyKit provides unified access to ComfyUI workflows: -- Local ComfyUI execution -- RunningHub cloud execution -- Flexible workflow-based generation -- Structured result handling - -Convention: Tool names must be image_{id} -""" - -import os -from typing import Any - -from comfykit import ComfyKit -from loguru import logger -from pydantic import Field - -from reelforge.core.mcp_server import reelforge_mcp - - -@reelforge_mcp.tool( - description="Generate images using ComfyKit (local ComfyUI or RunningHub cloud)", - meta={ - "reelforge": { - "display_name": "ComfyKit Image Generator", - "description": "基于 ComfyKit 的图像生成 - 支持本地和云端", - "is_default": True, - } - }, -) -async def image_comfykit( - workflow: str = Field(description="Workflow path, ID, or URL"), - comfyui_url: str | None = Field(default=None, description="ComfyUI server URL (default: http://127.0.0.1:8188)"), - runninghub_api_key: str | None = Field(default=None, description="RunningHub API key (for cloud execution)"), - # Common workflow parameters - prompt: str | None = Field(default=None, description="Image generation prompt"), - width: int | None = Field(default=None, description="Image width"), - height: int | None = Field(default=None, description="Image height"), - negative_prompt: str | None = Field(default=None, description="Negative prompt"), - steps: int | None = Field(default=None, description="Sampling steps"), - seed: int | None = Field(default=None, description="Random seed"), - cfg: float | None = Field(default=None, description="CFG scale"), - sampler: str | None = Field(default=None, description="Sampler name"), -) -> str: - """ - Generate image using ComfyKit - - Supports both local ComfyUI and RunningHub cloud execution. - Returns the first generated image URL. - - Environment variables (optional): - - COMFYUI_BASE_URL: ComfyUI server URL - - RUNNINGHUB_API_KEY: RunningHub API key - - Example: - # Local ComfyUI (default) - image_url = await image_comfykit( - workflow="workflows/t2i_by_local_flux.json", - prompt="A peaceful mountain landscape" - ) - - # RunningHub cloud - image_url = await image_comfykit( - workflow="12345", # RunningHub workflow ID - runninghub_api_key="rh-key-xxx", - prompt="a beautiful landscape" - ) - - # Custom ComfyUI server - image_url = await image_comfykit( - workflow="workflows/text2img.json", - comfyui_url="http://192.168.1.100:8188", - prompt="a cute cat" - ) - """ - logger.debug(f"Generating image with workflow: {workflow}") - - try: - # Initialize ComfyKit - kit_config = {} - - # Local ComfyUI configuration - if comfyui_url: - kit_config["comfyui_url"] = comfyui_url - elif os.getenv("COMFYUI_BASE_URL"): - kit_config["comfyui_url"] = os.getenv("COMFYUI_BASE_URL") - - # RunningHub cloud configuration - if runninghub_api_key: - kit_config["runninghub_api_key"] = runninghub_api_key - elif os.getenv("RUNNINGHUB_API_KEY"): - kit_config["runninghub_api_key"] = os.getenv("RUNNINGHUB_API_KEY") - - kit = ComfyKit(**kit_config) - - # Build workflow parameters - workflow_params = {} - if prompt is not None: - workflow_params["prompt"] = prompt - if width is not None: - workflow_params["width"] = width - if height is not None: - workflow_params["height"] = height - if negative_prompt is not None: - workflow_params["negative_prompt"] = negative_prompt - if steps is not None: - workflow_params["steps"] = steps - if seed is not None: - workflow_params["seed"] = seed - if cfg is not None: - workflow_params["cfg"] = cfg - if sampler is not None: - workflow_params["sampler"] = sampler - - logger.debug(f"Workflow parameters: {workflow_params}") - - # Execute workflow - result = await kit.execute(workflow, workflow_params) - - # Check execution status - if result.status != "completed": - error_msg = result.msg or "Unknown error" - logger.error(f"Image generation failed: {error_msg}") - raise Exception(f"Image generation failed: {error_msg}") - - # Return first image URL - if result.images: - image_url = result.images[0] - logger.info(f"Generated image: {image_url}") - return image_url - else: - logger.error("No images generated") - raise Exception("No images generated") - - except Exception as e: - logger.error(f"ComfyKit error: {e}") - raise - diff --git a/reelforge/service.py b/reelforge/service.py index 4d6646a..bb2b52d 100644 --- a/reelforge/service.py +++ b/reelforge/service.py @@ -12,7 +12,8 @@ from reelforge.config import load_config from reelforge.core.discovery import CapabilityRegistry from reelforge.core.mcp_server import reelforge_mcp from reelforge.core.config_manager import ConfigManager -from reelforge.services import LLMService, TTSService, ImageService +from reelforge.services import LLMService, TTSService +from reelforge.services.image import ImageService class ReelForgeCore: @@ -100,10 +101,12 @@ class ReelForgeCore: # 3. Create config manager self.config_manager = ConfigManager(self.registry, self.config) - # 4. Initialize services + # 4. Initialize capability-based services self.llm = LLMService(self.config_manager) self.tts = TTSService(self.config_manager) - self.image = ImageService(self.config_manager) + + # Initialize workflow-based services (no capability layer) + self.image = ImageService(self.config) # 5. Initialize content generation services from reelforge.services.narration_generator import NarrationGeneratorService @@ -134,7 +137,7 @@ class ReelForgeCore: # This triggers the @reelforge_mcp.tool decorators from reelforge.capabilities import llm # noqa: F401 from reelforge.capabilities import tts # noqa: F401 - from reelforge.capabilities import image # noqa: F401 + # Note: image no longer uses capability layer (workflow-based) @property def project_name(self) -> str: diff --git a/reelforge/services/image.py b/reelforge/services/image.py index ea9c1c0..87b78a1 100644 --- a/reelforge/services/image.py +++ b/reelforge/services/image.py @@ -1,74 +1,356 @@ """ -Image Generation Service +Image Generation Service - Workflow-based, no capability layer + +This service directly uses ComfyKit to execute workflows without going through +the capability abstraction layer. This is because workflow files themselves +already provide sufficient abstraction and flexibility. """ -from typing import Optional +import os +from pathlib import Path +from typing import Optional, List, Dict -from reelforge.services.base import BaseService +from comfykit import ComfyKit +from loguru import logger -class ImageService(BaseService): +class ImageService: """ - Image generation service + Image generation service - Workflow-based - Provides unified access to various image generation providers (ComfyKit, etc.) - Returns path or URL to generated image. + Directly uses ComfyKit to execute workflows. No capability abstraction needed + since workflow itself is already the abstraction. Usage: - # Direct call with workflow path - image_path = await reelforge.image( - workflow="workflows/t2i_by_local_flux.json", - prompt="A peaceful mountain landscape" - ) - # Returns: "http://comfyui.local/view?filename=..." + # Use default preset (workflows/image_default.json) + image_url = await reelforge.image(prompt="a cat") - # Or use workflow ID (if using RunningHub) - image_path = await reelforge.image( - workflow="12345", - prompt="a beautiful landscape" - ) + # Use specific preset + image_url = await reelforge.image(preset="flux", prompt="a cat") - # Check active image generator - print(f"Using: {reelforge.image.active}") + # List available presets + presets = reelforge.image.list_presets() + + # Get preset path + path = reelforge.image.get_preset_path("flux") """ - def __init__(self, router): - super().__init__(router, "image") + PRESET_PREFIX = "image_" + DEFAULT_PRESET = "default" + WORKFLOWS_DIR = "workflows" + + def __init__(self, config: dict): + """ + Initialize image service + + Args: + config: Full application config dict + """ + self.config = config.get("image", {}) + self._presets_cache: Optional[Dict[str, str]] = None + + def _scan_presets(self) -> Dict[str, str]: + """ + Scan workflows/image_*.json files + + Returns: + Dict mapping preset name to workflow path + Example: {"default": "workflows/image_default.json", "flux": "workflows/image_flux.json"} + """ + if self._presets_cache is not None: + return self._presets_cache + + presets = {} + workflows_dir = Path(self.WORKFLOWS_DIR) + + if not workflows_dir.exists(): + logger.warning(f"Workflows directory not found: {workflows_dir}") + return presets + + # Scan for image_*.json files + for file in workflows_dir.glob(f"{self.PRESET_PREFIX}*.json"): + # Extract preset name: "image_flux.json" -> "flux" + preset_name = file.stem.replace(self.PRESET_PREFIX, "") + presets[preset_name] = str(file) + logger.debug(f"Found image preset: {preset_name} -> {file}") + + self._presets_cache = presets + return presets + + def _get_default_preset(self) -> str: + """ + Get default preset name from config or use "default" + + Priority: + 1. config.yaml: image.default + 2. "default" + """ + return self.config.get("default", self.DEFAULT_PRESET) + + def _resolve_workflow( + self, + preset: Optional[str] = None, + workflow: Optional[str] = None + ) -> str: + """ + Resolve preset/workflow to actual workflow path + + Args: + preset: Preset name (e.g., "flux", "default") + workflow: Full workflow path (for backward compatibility) + + Returns: + Workflow file path + + Raises: + ValueError: If preset not found or no workflows available + """ + # 1. If explicit workflow path provided, use it + if workflow: + logger.debug(f"Using explicit workflow: {workflow}") + return workflow + + # 2. Scan available presets + presets = self._scan_presets() + + if not presets: + raise ValueError( + f"No workflow presets found in {self.WORKFLOWS_DIR}/ directory. " + f"Please create at least one workflow file: {self.WORKFLOWS_DIR}/{self.PRESET_PREFIX}default.json" + ) + + # 3. Determine which preset to use + if preset: + # Use specified preset + target_preset = preset + else: + # Use default preset + target_preset = self._get_default_preset() + + # 4. Lookup preset + if target_preset not in presets: + available = ", ".join(sorted(presets.keys())) + raise ValueError( + f"Preset '{target_preset}' not found. " + f"Available presets: {available}\n" + f"Please create: {self.WORKFLOWS_DIR}/{self.PRESET_PREFIX}{target_preset}.json" + ) + + workflow_path = presets[target_preset] + logger.info(f"🎨 Using image preset: {target_preset} ({workflow_path})") + + return workflow_path async def __call__( self, - workflow: str, + prompt: str, + preset: Optional[str] = None, + workflow: Optional[str] = None, + # ComfyUI connection (optional overrides) + comfyui_url: Optional[str] = None, + runninghub_api_key: Optional[str] = None, + # Common workflow parameters + width: Optional[int] = None, + height: Optional[int] = None, + negative_prompt: Optional[str] = None, + steps: Optional[int] = None, + seed: Optional[int] = None, + cfg: Optional[float] = None, + sampler: Optional[str] = None, **params ) -> str: """ Generate image using workflow Args: - workflow: Workflow path, ID, or URL - **params: Workflow parameters (e.g., prompt, title, author, etc.) + prompt: Image generation prompt + preset: Preset name (default: from config or "default") + workflow: Full workflow path (backward compatible) + comfyui_url: ComfyUI URL (optional, overrides config) + runninghub_api_key: RunningHub API key (optional, overrides config) + width: Image width + height: Image height + negative_prompt: Negative prompt + steps: Sampling steps + seed: Random seed + cfg: CFG scale + sampler: Sampler name + **params: Additional workflow parameters Returns: - Image URL or path (str) + Generated image URL/path - Example: - # Generate image + Examples: + # Simplest: use default preset (workflows/image_default.json) + image_url = await reelforge.image(prompt="a beautiful cat") + + # Use specific preset + image_url = await reelforge.image(preset="flux", prompt="a cat") + + # With additional parameters image_url = await reelforge.image( - workflow="workflows/t2i_by_local_flux.json", - prompt="A beautiful landscape", - author="James Clear", - genre="Self-Help" + preset="flux", + prompt="a cat", + width=1024, + height=1024, + steps=20, + seed=42 ) - # Generate from text prompt + # Backward compatible: direct workflow path image_url = await reelforge.image( - workflow="workflows/text2img.json", - prompt="a cute cat playing with yarn", - width=1024, - height=768 + workflow="workflows/custom.json", + prompt="a cat" + ) + + # With custom ComfyUI server + image_url = await reelforge.image( + prompt="a cat", + comfyui_url="http://192.168.1.100:8188" ) """ - call_params = {"workflow": workflow} - call_params.update(params) + # 1. Resolve workflow path + workflow_path = self._resolve_workflow(preset=preset, workflow=workflow) - return await self._config_manager.call(self._capability_type, **call_params) - + # 2. Prepare ComfyKit config + kit_config = {} + + # ComfyUI URL (priority: param > config > env > default) + final_comfyui_url = ( + comfyui_url + or self.config.get("comfyui_url") + or os.getenv("COMFYUI_BASE_URL") + or "http://127.0.0.1:8188" + ) + kit_config["comfyui_url"] = final_comfyui_url + + # RunningHub API key (priority: param > config > env) + final_rh_key = ( + runninghub_api_key + or self.config.get("runninghub_api_key") + or os.getenv("RUNNINGHUB_API_KEY") + ) + if final_rh_key: + kit_config["runninghub_api_key"] = final_rh_key + + logger.debug(f"ComfyKit config: {kit_config}") + + # 3. Build workflow parameters + workflow_params = {"prompt": prompt} + + # Add optional parameters + if width is not None: + workflow_params["width"] = width + if height is not None: + workflow_params["height"] = height + if negative_prompt is not None: + workflow_params["negative_prompt"] = negative_prompt + if steps is not None: + workflow_params["steps"] = steps + if seed is not None: + workflow_params["seed"] = seed + if cfg is not None: + workflow_params["cfg"] = cfg + if sampler is not None: + workflow_params["sampler"] = sampler + + # Add any additional parameters + workflow_params.update(params) + + logger.debug(f"Workflow parameters: {workflow_params}") + + # 4. Execute workflow + try: + kit = ComfyKit(**kit_config) + + logger.info(f"Executing workflow: {workflow_path}") + result = await kit.execute(workflow_path, workflow_params) + + # 5. Handle result + if result.status != "completed": + error_msg = result.msg or "Unknown error" + logger.error(f"Image generation failed: {error_msg}") + raise Exception(f"Image generation failed: {error_msg}") + + if not result.images: + logger.error("No images generated") + raise Exception("No images generated") + + image_url = result.images[0] + logger.info(f"✅ Generated image: {image_url}") + return image_url + + except Exception as e: + logger.error(f"Image generation error: {e}") + raise + + def list_presets(self) -> List[str]: + """ + List all available image presets + + Returns: + List of preset names (sorted alphabetically) + + Example: + presets = reelforge.image.list_presets() + # ['anime', 'default', 'flux', 'sd15'] + """ + return sorted(self._scan_presets().keys()) + + def get_preset_path(self, preset: str) -> Optional[str]: + """ + Get workflow path for a preset + + Args: + preset: Preset name + + Returns: + Workflow file path, or None if not found + + Example: + path = reelforge.image.get_preset_path("flux") + # 'workflows/image_flux.json' + """ + return self._scan_presets().get(preset) + + @property + def active(self) -> str: + """ + Get active preset name + + This property is provided for compatibility with other services + that use the capability layer. + + Returns: + Active preset name + + Example: + print(f"Using preset: {reelforge.image.active}") + """ + return self._get_default_preset() + + @property + def available(self) -> List[str]: + """ + List available presets + + This property is provided for compatibility with other services + that use the capability layer. + + Returns: + List of available preset names + + Example: + print(f"Available presets: {reelforge.image.available}") + """ + return self.list_presets() + + def __repr__(self) -> str: + """String representation""" + active = self.active + available = ", ".join(self.available) if self.available else "none" + return ( + f"" + ) diff --git a/reelforge/services/storyboard_processor.py b/reelforge/services/storyboard_processor.py index 81ce27b..268095a 100644 --- a/reelforge/services/storyboard_processor.py +++ b/reelforge/services/storyboard_processor.py @@ -140,9 +140,8 @@ class StoryboardProcessorService: """Step 2: Generate image using ComfyKit""" logger.debug(f" 2/4: Generating image for frame {frame.index}...") - # Call Image generation + # Call Image generation (using default preset) image_url = await self.core.image( - workflow="workflows/t2i_by_local_flux.json", # Use existing workflow prompt=frame.image_prompt, width=config.image_width, height=config.image_height diff --git a/web.py b/web.py index 9b8e340..f6a4b72 100644 --- a/web.py +++ b/web.py @@ -111,8 +111,8 @@ def generate_style_preview_cached( )) # Generate preview image (small size for speed) + # Using default preset (workflows/image_default.json) preview_image_path = run_async(reelforge.image( - workflow="workflows/t2i_by_local_flux.json", prompt=final_prompt, width=512, height=512 diff --git a/workflows/t2i_by_local_flux.json b/workflows/image_default.json similarity index 100% rename from workflows/t2i_by_local_flux.json rename to workflows/image_default.json