改造image自定义逻辑

This commit is contained in:
puke
2025-10-26 13:12:21 +08:00
committed by puke
parent 0bce687e0c
commit a9d793749f
7 changed files with 337 additions and 188 deletions

5
.gitignore vendored
View File

@@ -67,3 +67,8 @@ output
plans/
examples/
repositories/
# Workflows - ignore user customizations but keep defaults
workflows/*
!workflows/*_default.json
!workflows/README.md

View File

@@ -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

View File

@@ -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:

View File

@@ -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}]>"
)

View File

@@ -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
View File

@@ -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