改造image自定义逻辑
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -67,3 +67,8 @@ output
|
||||
plans/
|
||||
examples/
|
||||
repositories/
|
||||
|
||||
# Workflows - ignore user customizations but keep defaults
|
||||
workflows/*
|
||||
!workflows/*_default.json
|
||||
!workflows/README.md
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"<ImageService "
|
||||
f"active={active!r} "
|
||||
f"available=[{available}]>"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
2
web.py
2
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
|
||||
|
||||
Reference in New Issue
Block a user