更新配置文件,重构工作流管理逻辑,调整国际化文件文职,优化图像生成和文本转语音服务的工作流解析,确保默认工作流配置必填,调整前端工作流选择逻辑。
This commit is contained in:
@@ -23,7 +23,16 @@ tts:
|
||||
|
||||
# ==================== Image Generation Configuration ====================
|
||||
image:
|
||||
default: image_default.json
|
||||
# Required: Default workflow to use (no fallback)
|
||||
# Options: runninghub/image_default.json (recommended, no local setup)
|
||||
# selfhost/image_default.json (requires local ComfyUI)
|
||||
default_workflow: runninghub/image_default.json
|
||||
|
||||
# Local ComfyUI configuration (required if using selfhost workflows)
|
||||
comfyui_url: http://127.0.0.1:8188
|
||||
|
||||
# RunningHub cloud configuration (required if using runninghub workflows)
|
||||
runninghub_api_key: ""
|
||||
|
||||
# Image prompt prefix (optional)
|
||||
prompt_prefix: "Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines"
|
||||
|
||||
@@ -96,7 +96,7 @@ class ConfigManager:
|
||||
def get_image_config(self) -> dict:
|
||||
"""Get image configuration as dict"""
|
||||
return {
|
||||
"default": self.config.image.default,
|
||||
"default_workflow": self.config.image.default_workflow,
|
||||
"comfyui_url": self.config.image.comfyui_url,
|
||||
"runninghub_api_key": self.config.image.runninghub_api_key,
|
||||
"prompt_prefix": self.config.image.prompt_prefix,
|
||||
|
||||
@@ -15,12 +15,16 @@ class LLMConfig(BaseModel):
|
||||
|
||||
class TTSConfig(BaseModel):
|
||||
"""TTS configuration"""
|
||||
default: str = Field(default="edge", description="Default TTS workflow")
|
||||
model_config = {"populate_by_name": True} # Allow both field name and alias
|
||||
|
||||
default_workflow: str = Field(default="edge", description="Default TTS workflow", alias="default")
|
||||
|
||||
|
||||
class ImageConfig(BaseModel):
|
||||
"""Image generation configuration"""
|
||||
default: str = Field(default="image_default.json", description="Default image workflow")
|
||||
model_config = {"populate_by_name": True} # Allow both field name and alias
|
||||
|
||||
default_workflow: str = Field(default=None, description="Default image workflow (required, no fallback)", alias="default")
|
||||
comfyui_url: str = Field(default="http://127.0.0.1:8188", description="ComfyUI Server URL")
|
||||
runninghub_api_key: str = Field(default="", description="RunningHub API Key (optional)")
|
||||
prompt_prefix: str = Field(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
ComfyUI Base Service - Common logic for ComfyUI-based services
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
@@ -38,13 +39,29 @@ class ComfyBaseService:
|
||||
self.service_name = service_name
|
||||
self._workflows_cache: Optional[List[str]] = None
|
||||
|
||||
def _scan_workflows(self) -> List[str]:
|
||||
def _scan_workflows(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Scan workflows/{prefix}*.json files
|
||||
Scan workflows/source/*.json files from all source directories
|
||||
|
||||
Returns:
|
||||
List of workflow filenames
|
||||
Example: ["image_default.json", "image_flux.json"]
|
||||
List of workflow info dicts
|
||||
Example: [
|
||||
{
|
||||
"name": "image_default.json",
|
||||
"display_name": "image_default.json - Selfhost",
|
||||
"source": "selfhost",
|
||||
"path": "workflows/selfhost/image_default.json",
|
||||
"key": "selfhost/image_default.json"
|
||||
},
|
||||
{
|
||||
"name": "image_default.json",
|
||||
"display_name": "image_default.json - Runninghub",
|
||||
"source": "runninghub",
|
||||
"path": "workflows/runninghub/image_default.json",
|
||||
"key": "runninghub/image_default.json",
|
||||
"workflow_id": "123456"
|
||||
}
|
||||
]
|
||||
"""
|
||||
workflows = []
|
||||
workflows_dir = Path(self.WORKFLOWS_DIR)
|
||||
@@ -53,67 +70,130 @@ class ComfyBaseService:
|
||||
logger.warning(f"Workflows directory not found: {workflows_dir}")
|
||||
return workflows
|
||||
|
||||
# Scan for {prefix}_*.json files
|
||||
for file in workflows_dir.glob(f"{self.WORKFLOW_PREFIX}*.json"):
|
||||
workflows.append(file.name)
|
||||
logger.debug(f"Found {self.service_name} workflow: {file.name}")
|
||||
# Scan subdirectories (selfhost, runninghub, etc.)
|
||||
for source_dir in workflows_dir.iterdir():
|
||||
if not source_dir.is_dir():
|
||||
logger.debug(f"Skipping non-directory: {source_dir}")
|
||||
continue
|
||||
|
||||
source_name = source_dir.name
|
||||
|
||||
# Scan workflow files in this source directory
|
||||
for file_path in source_dir.glob(f"{self.WORKFLOW_PREFIX}*.json"):
|
||||
try:
|
||||
workflow_info = self._parse_workflow_file(file_path, source_name)
|
||||
workflows.append(workflow_info)
|
||||
logger.debug(f"Found workflow: {workflow_info['key']}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse workflow {file_path}: {e}")
|
||||
|
||||
return sorted(workflows)
|
||||
# Sort by key (source/name)
|
||||
return sorted(workflows, key=lambda w: w["key"])
|
||||
|
||||
def _parse_workflow_file(self, file_path: Path, source: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse workflow file and extract metadata
|
||||
|
||||
Args:
|
||||
file_path: Path to workflow JSON file
|
||||
source: Source directory name (e.g., "selfhost", "runninghub")
|
||||
|
||||
Returns:
|
||||
Workflow info dict with structure:
|
||||
{
|
||||
"name": "image_default.json",
|
||||
"display_name": "image_default.json - Runninghub",
|
||||
"source": "runninghub",
|
||||
"path": "workflows/runninghub/image_default.json",
|
||||
"key": "runninghub/image_default.json",
|
||||
"workflow_id": "123456" # Only for RunningHub
|
||||
}
|
||||
"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = json.load(f)
|
||||
|
||||
# Build base info
|
||||
workflow_info = {
|
||||
"name": file_path.name,
|
||||
"display_name": f"{file_path.name} - {source.title()}",
|
||||
"source": source,
|
||||
"path": str(file_path),
|
||||
"key": f"{source}/{file_path.name}"
|
||||
}
|
||||
|
||||
# Check if it's a wrapper format (RunningHub, etc.)
|
||||
if "source" in content:
|
||||
# Wrapper format: {"source": "runninghub", "workflow_id": "xxx", ...}
|
||||
if "workflow_id" in content:
|
||||
workflow_info["workflow_id"] = content["workflow_id"]
|
||||
if "metadata" in content:
|
||||
workflow_info["metadata"] = content["metadata"]
|
||||
|
||||
return workflow_info
|
||||
|
||||
def _get_default_workflow(self) -> str:
|
||||
"""
|
||||
Get default workflow name from config or use DEFAULT_WORKFLOW
|
||||
Get default workflow from config (required, no fallback)
|
||||
|
||||
Returns:
|
||||
Default workflow filename
|
||||
Default workflow key (e.g., "runninghub/image_default.json")
|
||||
|
||||
Raises:
|
||||
ValueError: If default_workflow not configured
|
||||
"""
|
||||
return self.config.get("default_workflow", self.DEFAULT_WORKFLOW)
|
||||
default_workflow = self.config.get("default_workflow")
|
||||
|
||||
if not default_workflow:
|
||||
raise ValueError(
|
||||
f"No default workflow configured for {self.service_name}. "
|
||||
f"Please set 'default_workflow' in config.yaml under '{self.service_name}' section. "
|
||||
f"Available workflows: {', '.join(self.available)}"
|
||||
)
|
||||
|
||||
return default_workflow
|
||||
|
||||
def _resolve_workflow(self, workflow: Optional[str] = None) -> str:
|
||||
def _resolve_workflow(self, workflow: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve workflow to actual workflow path
|
||||
Resolve workflow key to workflow info
|
||||
|
||||
Args:
|
||||
workflow: Workflow filename (e.g., "image_default.json")
|
||||
Can also be:
|
||||
- Absolute path: "/path/to/workflow.json"
|
||||
- Relative path: "custom/workflow.json"
|
||||
- URL: "http://..."
|
||||
- RunningHub ID: "12345"
|
||||
workflow: Workflow key (e.g., "runninghub/image_default.json")
|
||||
If None, uses default from config
|
||||
|
||||
Returns:
|
||||
Workflow file path or identifier
|
||||
Workflow info dict with structure:
|
||||
{
|
||||
"name": "image_default.json",
|
||||
"display_name": "image_default.json - Runninghub",
|
||||
"source": "runninghub",
|
||||
"path": "workflows/runninghub/image_default.json",
|
||||
"key": "runninghub/image_default.json",
|
||||
"workflow_id": "123456" # Only for RunningHub
|
||||
}
|
||||
|
||||
Raises:
|
||||
ValueError: If workflow not found
|
||||
"""
|
||||
# 1. If not specified, use default
|
||||
# 1. If not specified, use default from config
|
||||
if workflow is None:
|
||||
workflow = self._get_default_workflow()
|
||||
|
||||
# 2. If it's an absolute path, URL, or looks like RunningHub ID, use as-is
|
||||
if (workflow.startswith("/") or
|
||||
workflow.startswith("http://") or
|
||||
workflow.startswith("https://") or
|
||||
workflow.isdigit()):
|
||||
logger.debug(f"Using workflow identifier: {workflow}")
|
||||
return workflow
|
||||
# 2. Scan available workflows
|
||||
available_workflows = self._scan_workflows()
|
||||
|
||||
# 3. If it's just a filename, look in workflows/ directory
|
||||
workflow_path = Path(self.WORKFLOWS_DIR) / workflow
|
||||
# 3. Find matching workflow by key
|
||||
for wf_info in available_workflows:
|
||||
if wf_info["key"] == workflow:
|
||||
logger.info(f"🎬 Using {self.service_name} workflow: {workflow}")
|
||||
return wf_info
|
||||
|
||||
if not workflow_path.exists():
|
||||
# List available workflows for error message
|
||||
available = self._scan_workflows()
|
||||
available_str = ", ".join(available) if available else "none"
|
||||
raise ValueError(
|
||||
f"Workflow '{workflow}' not found at {workflow_path}. "
|
||||
f"Available workflows: {available_str}\n"
|
||||
f"Please create: {workflow_path}"
|
||||
)
|
||||
|
||||
logger.info(f"🎬 Using {self.service_name} workflow: {workflow}")
|
||||
return str(workflow_path)
|
||||
# 4. Not found - generate error message
|
||||
available_keys = [wf["key"] for wf in available_workflows]
|
||||
available_str = ", ".join(available_keys) if available_keys else "none"
|
||||
raise ValueError(
|
||||
f"Workflow '{workflow}' not found. "
|
||||
f"Available workflows: {available_str}"
|
||||
)
|
||||
|
||||
def _prepare_comfykit_config(
|
||||
self,
|
||||
@@ -153,31 +233,42 @@ class ComfyBaseService:
|
||||
logger.debug(f"ComfyKit config: {kit_config}")
|
||||
return kit_config
|
||||
|
||||
def list_workflows(self) -> List[str]:
|
||||
def list_workflows(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all available workflows
|
||||
List all available workflows with full metadata
|
||||
|
||||
Returns:
|
||||
List of workflow filenames (sorted alphabetically)
|
||||
List of workflow info dicts (sorted by key)
|
||||
|
||||
Example:
|
||||
workflows = service.list_workflows()
|
||||
# ['image_default.json', 'image_flux.json']
|
||||
# [
|
||||
# {
|
||||
# "name": "image_default.json",
|
||||
# "display_name": "image_default.json - Runninghub",
|
||||
# "source": "runninghub",
|
||||
# "path": "workflows/runninghub/image_default.json",
|
||||
# "key": "runninghub/image_default.json",
|
||||
# "workflow_id": "123456"
|
||||
# },
|
||||
# ...
|
||||
# ]
|
||||
"""
|
||||
return self._scan_workflows()
|
||||
|
||||
@property
|
||||
def available(self) -> List[str]:
|
||||
"""
|
||||
List available workflows
|
||||
List available workflow keys
|
||||
|
||||
Returns:
|
||||
List of available workflow filenames
|
||||
List of available workflow keys (e.g., ["runninghub/image_default.json", ...])
|
||||
|
||||
Example:
|
||||
print(f"Available workflows: {service.available}")
|
||||
"""
|
||||
return self.list_workflows()
|
||||
workflows = self.list_workflows()
|
||||
return [wf["key"] for wf in workflows]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation"""
|
||||
|
||||
@@ -31,7 +31,7 @@ class ImageService(ComfyBaseService):
|
||||
"""
|
||||
|
||||
WORKFLOW_PREFIX = "image_"
|
||||
DEFAULT_WORKFLOW = "image_default.json"
|
||||
DEFAULT_WORKFLOW = None # No hardcoded default, must be configured
|
||||
WORKFLOWS_DIR = "workflows"
|
||||
|
||||
def __init__(self, config: dict):
|
||||
@@ -112,10 +112,10 @@ class ImageService(ComfyBaseService):
|
||||
comfyui_url="http://192.168.1.100:8188"
|
||||
)
|
||||
"""
|
||||
# 1. Resolve workflow path
|
||||
workflow_path = self._resolve_workflow(workflow=workflow)
|
||||
# 1. Resolve workflow (returns structured info)
|
||||
workflow_info = self._resolve_workflow(workflow=workflow)
|
||||
|
||||
# 2. Prepare ComfyKit config
|
||||
# 2. Prepare ComfyKit config (supports both selfhost and runninghub)
|
||||
kit_config = self._prepare_comfykit_config(
|
||||
comfyui_url=comfyui_url,
|
||||
runninghub_api_key=runninghub_api_key
|
||||
@@ -145,12 +145,21 @@ class ImageService(ComfyBaseService):
|
||||
|
||||
logger.debug(f"Workflow parameters: {workflow_params}")
|
||||
|
||||
# 4. Execute workflow
|
||||
# 4. Execute workflow (ComfyKit auto-detects based on input type)
|
||||
try:
|
||||
kit = ComfyKit(**kit_config)
|
||||
|
||||
logger.info(f"Executing workflow: {workflow_path}")
|
||||
result = await kit.execute(workflow_path, workflow_params)
|
||||
# Determine what to pass to ComfyKit based on source
|
||||
if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:
|
||||
# RunningHub: pass workflow_id (ComfyKit will use runninghub backend)
|
||||
workflow_input = workflow_info["workflow_id"]
|
||||
logger.info(f"Executing RunningHub workflow: {workflow_input}")
|
||||
else:
|
||||
# Selfhost: pass file path (ComfyKit will use local ComfyUI)
|
||||
workflow_input = workflow_info["path"]
|
||||
logger.info(f"Executing selfhost workflow: {workflow_input}")
|
||||
|
||||
result = await kit.execute(workflow_input, workflow_params)
|
||||
|
||||
# 5. Handle result
|
||||
if result.status != "completed":
|
||||
|
||||
@@ -139,11 +139,8 @@ class TTSService(ComfyBaseService):
|
||||
workflow="/path/to/custom_tts.json"
|
||||
)
|
||||
"""
|
||||
# 1. Resolve workflow path or provider
|
||||
workflow_or_provider = self._resolve_workflow(workflow=workflow)
|
||||
|
||||
# 2. Determine execution path
|
||||
if workflow_or_provider in self.BUILTIN_PROVIDERS:
|
||||
# 1. Check if it's a builtin provider (edge-tts)
|
||||
if workflow in self.BUILTIN_PROVIDERS or workflow is None and self._get_default_workflow() in self.BUILTIN_PROVIDERS:
|
||||
# Use edge-tts
|
||||
return await self._call_edge_tts(
|
||||
text=text,
|
||||
@@ -154,20 +151,22 @@ class TTSService(ComfyBaseService):
|
||||
output_path=output_path,
|
||||
**params
|
||||
)
|
||||
else:
|
||||
# Use ComfyUI workflow
|
||||
return await self._call_comfyui_workflow(
|
||||
workflow_path=workflow_or_provider,
|
||||
text=text,
|
||||
comfyui_url=comfyui_url,
|
||||
runninghub_api_key=runninghub_api_key,
|
||||
voice=voice,
|
||||
rate=rate,
|
||||
volume=volume,
|
||||
pitch=pitch,
|
||||
output_path=output_path,
|
||||
**params
|
||||
)
|
||||
|
||||
# 2. Use ComfyUI workflow - resolve to structured info
|
||||
workflow_info = self._resolve_workflow(workflow=workflow)
|
||||
|
||||
return await self._call_comfyui_workflow(
|
||||
workflow_info=workflow_info,
|
||||
text=text,
|
||||
comfyui_url=comfyui_url,
|
||||
runninghub_api_key=runninghub_api_key,
|
||||
voice=voice,
|
||||
rate=rate,
|
||||
volume=volume,
|
||||
pitch=pitch,
|
||||
output_path=output_path,
|
||||
**params
|
||||
)
|
||||
|
||||
async def _call_edge_tts(
|
||||
self,
|
||||
@@ -227,7 +226,7 @@ class TTSService(ComfyBaseService):
|
||||
|
||||
async def _call_comfyui_workflow(
|
||||
self,
|
||||
workflow_path: str,
|
||||
workflow_info: dict,
|
||||
text: str,
|
||||
comfyui_url: Optional[str] = None,
|
||||
runninghub_api_key: Optional[str] = None,
|
||||
@@ -242,7 +241,7 @@ class TTSService(ComfyBaseService):
|
||||
Generate speech using ComfyUI workflow
|
||||
|
||||
Args:
|
||||
workflow_path: Path to workflow file
|
||||
workflow_info: Workflow info dict from _resolve_workflow()
|
||||
text: Text to convert to speech
|
||||
comfyui_url: ComfyUI URL
|
||||
runninghub_api_key: RunningHub API key
|
||||
@@ -256,9 +255,9 @@ class TTSService(ComfyBaseService):
|
||||
Returns:
|
||||
Generated audio file path (local if output_path provided, otherwise URL)
|
||||
"""
|
||||
logger.info(f"🎙️ Using ComfyUI workflow: {workflow_path}")
|
||||
logger.info(f"🎙️ Using workflow: {workflow_info['key']}")
|
||||
|
||||
# 1. Prepare ComfyKit config
|
||||
# 1. Prepare ComfyKit config (supports both selfhost and runninghub)
|
||||
kit_config = self._prepare_comfykit_config(
|
||||
comfyui_url=comfyui_url,
|
||||
runninghub_api_key=runninghub_api_key
|
||||
@@ -282,12 +281,21 @@ class TTSService(ComfyBaseService):
|
||||
|
||||
logger.debug(f"Workflow parameters: {workflow_params}")
|
||||
|
||||
# 3. Execute workflow
|
||||
# 3. Execute workflow (ComfyKit auto-detects based on input type)
|
||||
try:
|
||||
kit = ComfyKit(**kit_config)
|
||||
|
||||
logger.info(f"Executing TTS workflow: {workflow_path}")
|
||||
result = await kit.execute(workflow_path, workflow_params)
|
||||
# Determine what to pass to ComfyKit based on source
|
||||
if workflow_info["source"] == "runninghub" and "workflow_id" in workflow_info:
|
||||
# RunningHub: pass workflow_id
|
||||
workflow_input = workflow_info["workflow_id"]
|
||||
logger.info(f"Executing RunningHub TTS workflow: {workflow_input}")
|
||||
else:
|
||||
# Selfhost: pass file path
|
||||
workflow_input = workflow_info["path"]
|
||||
logger.info(f"Executing selfhost TTS workflow: {workflow_input}")
|
||||
|
||||
result = await kit.execute(workflow_input, workflow_params)
|
||||
|
||||
# 4. Handle result
|
||||
if result.status != "completed":
|
||||
|
||||
151
web/app.py
151
web/app.py
@@ -13,7 +13,7 @@ import streamlit as st
|
||||
from loguru import logger
|
||||
|
||||
# Import i18n and config manager
|
||||
from reelforge.i18n import load_locales, set_language, tr, get_available_languages
|
||||
from web.i18n import load_locales, set_language, tr, get_available_languages
|
||||
from reelforge.config import config_manager
|
||||
from reelforge.models.progress import ProgressEvent
|
||||
|
||||
@@ -63,38 +63,6 @@ def init_i18n():
|
||||
set_language(st.session_state.language)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Preview Cache Functions
|
||||
# ============================================================================
|
||||
|
||||
def generate_style_preview_cached(prompt_prefix: str):
|
||||
"""
|
||||
Generate and cache visual style preview
|
||||
|
||||
Args:
|
||||
prompt_prefix: Prompt prefix to test
|
||||
|
||||
Returns:
|
||||
Generated image path
|
||||
"""
|
||||
from reelforge.utils.prompt_helper import build_image_prompt
|
||||
|
||||
reelforge = get_reelforge()
|
||||
|
||||
# Build final prompt with prefix
|
||||
test_prompt = "A peaceful mountain landscape"
|
||||
final_prompt = build_image_prompt(test_prompt, prompt_prefix)
|
||||
|
||||
# Generate preview image (small size for speed)
|
||||
preview_image_path = run_async(reelforge.image(
|
||||
prompt=final_prompt,
|
||||
width=512,
|
||||
height=512
|
||||
))
|
||||
|
||||
return preview_image_path
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Initialize ReelForge
|
||||
# ============================================================================
|
||||
@@ -512,25 +480,39 @@ def main():
|
||||
st.caption(tr("style.workflow"))
|
||||
st.caption(tr("style.workflow_help"))
|
||||
|
||||
# Dynamically scan workflows folder for image_*.json files
|
||||
workflows_folder = Path("workflows")
|
||||
workflow_files = []
|
||||
if workflows_folder.exists():
|
||||
workflow_files = sorted([f.name for f in workflows_folder.glob("image_*.json")])
|
||||
# Get available workflows from reelforge (with source info)
|
||||
workflows = reelforge.image.list_workflows()
|
||||
|
||||
# Default to "image_default.json" if exists, otherwise first option
|
||||
# Build options for selectbox
|
||||
# Display: "image_default.json - Runninghub"
|
||||
# Value: "runninghub/image_default.json"
|
||||
workflow_options = [wf["display_name"] for wf in workflows]
|
||||
workflow_keys = [wf["key"] for wf in workflows]
|
||||
|
||||
# Default to first option (should be runninghub by sorting)
|
||||
default_workflow_index = 0
|
||||
if "image_default.json" in workflow_files:
|
||||
default_workflow_index = workflow_files.index("image_default.json")
|
||||
|
||||
workflow_filename = st.selectbox(
|
||||
# If user has a saved preference in config, try to match it
|
||||
image_config = config_manager.get_image_config()
|
||||
saved_workflow = image_config.get("default_workflow")
|
||||
if saved_workflow and saved_workflow in workflow_keys:
|
||||
default_workflow_index = workflow_keys.index(saved_workflow)
|
||||
|
||||
workflow_display = st.selectbox(
|
||||
"Workflow",
|
||||
workflow_files if workflow_files else ["image_default.json"],
|
||||
workflow_options if workflow_options else ["No workflows found"],
|
||||
index=default_workflow_index,
|
||||
label_visibility="collapsed",
|
||||
key="image_workflow_select"
|
||||
)
|
||||
|
||||
# Get the actual workflow key (e.g., "runninghub/image_default.json")
|
||||
if workflow_options:
|
||||
workflow_selected_index = workflow_options.index(workflow_display)
|
||||
workflow_key = workflow_keys[workflow_selected_index]
|
||||
else:
|
||||
workflow_key = "runninghub/image_default.json" # fallback
|
||||
|
||||
|
||||
# 2. Prompt prefix input
|
||||
st.caption(tr("style.prompt_prefix"))
|
||||
@@ -549,38 +531,59 @@ def main():
|
||||
help=tr("style.prompt_prefix_help")
|
||||
)
|
||||
|
||||
# Visual style preview button
|
||||
if st.button(tr("style.preview"), key="preview_style", use_container_width=True):
|
||||
with st.spinner(tr("style.previewing")):
|
||||
try:
|
||||
# Generate preview using cached function
|
||||
preview_image_path = generate_style_preview_cached(prompt_prefix)
|
||||
|
||||
# Display preview (support both URL and local path)
|
||||
if preview_image_path:
|
||||
# Read and encode image
|
||||
if preview_image_path.startswith('http'):
|
||||
# URL - use directly
|
||||
img_html = f'<div class="preview-image"><img src="{preview_image_path}" alt="Style Preview"/></div>'
|
||||
else:
|
||||
# Local file - encode as base64
|
||||
with open(preview_image_path, 'rb') as f:
|
||||
img_data = base64.b64encode(f.read()).decode()
|
||||
img_html = f'<div class="preview-image"><img src="data:image/png;base64,{img_data}" alt="Style Preview"/></div>'
|
||||
|
||||
st.markdown(img_html, unsafe_allow_html=True)
|
||||
st.caption("Preview with test prompt: 'A peaceful mountain landscape'")
|
||||
|
||||
# Show the final prompt used
|
||||
# Style preview expander (similar to template preview)
|
||||
with st.expander(tr("style.preview_title"), expanded=False):
|
||||
# Test prompt input
|
||||
test_prompt = st.text_input(
|
||||
tr("style.test_prompt"),
|
||||
value="a dog",
|
||||
help=tr("style.test_prompt_help"),
|
||||
key="style_test_prompt"
|
||||
)
|
||||
|
||||
# Preview button
|
||||
if st.button(tr("style.preview"), key="preview_style", use_container_width=True):
|
||||
with st.spinner(tr("style.previewing")):
|
||||
try:
|
||||
from reelforge.utils.prompt_helper import build_image_prompt
|
||||
test_prompt = "A peaceful mountain landscape"
|
||||
|
||||
# Build final prompt with prefix
|
||||
final_prompt = build_image_prompt(test_prompt, prompt_prefix)
|
||||
st.info(f"Final prompt used: {final_prompt}")
|
||||
else:
|
||||
st.error("Failed to generate preview image")
|
||||
except Exception as e:
|
||||
st.error(tr("style.preview_failed", error=str(e)))
|
||||
logger.exception(e)
|
||||
|
||||
# Generate preview image (small size for speed)
|
||||
preview_image_path = run_async(reelforge.image(
|
||||
prompt=final_prompt,
|
||||
workflow=workflow_key,
|
||||
width=512,
|
||||
height=512
|
||||
))
|
||||
|
||||
# Display preview (support both URL and local path)
|
||||
if preview_image_path:
|
||||
st.success(tr("style.preview_success"))
|
||||
|
||||
# Read and encode image
|
||||
if preview_image_path.startswith('http'):
|
||||
# URL - use directly
|
||||
img_html = f'<div class="preview-image"><img src="{preview_image_path}" alt="Style Preview"/></div>'
|
||||
else:
|
||||
# Local file - encode as base64
|
||||
with open(preview_image_path, 'rb') as f:
|
||||
img_data = base64.b64encode(f.read()).decode()
|
||||
img_html = f'<div class="preview-image"><img src="data:image/png;base64,{img_data}" alt="Style Preview"/></div>'
|
||||
|
||||
st.markdown(img_html, unsafe_allow_html=True)
|
||||
|
||||
# Show the final prompt used
|
||||
st.info(f"**{tr('style.final_prompt_label')}**\n{final_prompt}")
|
||||
|
||||
# Show file path
|
||||
st.caption(f"📁 {preview_image_path}")
|
||||
else:
|
||||
st.error(tr("style.preview_failed_general"))
|
||||
except Exception as e:
|
||||
st.error(tr("style.preview_failed", error=str(e)))
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
# Frame template (moved from right column)
|
||||
@@ -755,7 +758,7 @@ def main():
|
||||
title=title if title else None,
|
||||
n_scenes=n_scenes,
|
||||
voice_id=voice_id,
|
||||
image_workflow=workflow_filename, # Pass image workflow filename
|
||||
image_workflow=workflow_key, # Pass workflow key (e.g., "runninghub/image_default.json")
|
||||
frame_template=frame_template,
|
||||
prompt_prefix=prompt_prefix, # Pass prompt_prefix
|
||||
bgm_path=bgm_path,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
International language support for ReelForge
|
||||
International language support for ReelForge Web UI
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -65,10 +65,16 @@
|
||||
"style.custom": "Custom",
|
||||
"style.description": "Style Description",
|
||||
"style.description_placeholder": "Describe the illustration style you want (any language)...",
|
||||
"style.preview": "🖼️ Preview Style",
|
||||
"style.preview_title": "🔍 Preview Style",
|
||||
"style.test_prompt": "Test Prompt",
|
||||
"style.test_prompt_help": "Enter test prompt to preview style effect",
|
||||
"style.preview": "🖼️ Generate Preview",
|
||||
"style.previewing": "Generating style preview...",
|
||||
"style.preview_success": "✅ Preview generated successfully!",
|
||||
"style.preview_caption": "Style Preview",
|
||||
"style.preview_failed": "Preview failed: {error}",
|
||||
"style.preview_failed_general": "Failed to generate preview image",
|
||||
"style.final_prompt_label": "Final Prompt",
|
||||
"style.generated_prompt": "Generated prompt: {prompt}",
|
||||
|
||||
"template.title": "📐 Storyboard Template",
|
||||
@@ -65,10 +65,16 @@
|
||||
"style.custom": "自定义",
|
||||
"style.description": "风格描述",
|
||||
"style.description_placeholder": "描述您想要的插图风格(任何语言)...",
|
||||
"style.preview": "🖼️ 预览风格",
|
||||
"style.preview_title": "🔍 预览风格",
|
||||
"style.test_prompt": "测试提示词",
|
||||
"style.test_prompt_help": "输入测试提示词来预览风格效果",
|
||||
"style.preview": "🖼️ 生成预览",
|
||||
"style.previewing": "正在生成风格预览...",
|
||||
"style.preview_success": "✅ 预览生成成功!",
|
||||
"style.preview_caption": "风格预览",
|
||||
"style.preview_failed": "预览失败:{error}",
|
||||
"style.preview_failed_general": "预览图片生成失败",
|
||||
"style.final_prompt_label": "最终提示词",
|
||||
"style.generated_prompt": "生成的提示词:{prompt}",
|
||||
|
||||
"template.title": "📐 分镜模板",
|
||||
@@ -1,208 +0,0 @@
|
||||
{
|
||||
"2": {
|
||||
"inputs": {
|
||||
"noise": [
|
||||
"5",
|
||||
0
|
||||
],
|
||||
"guider": [
|
||||
"7",
|
||||
0
|
||||
],
|
||||
"sampler": [
|
||||
"4",
|
||||
0
|
||||
],
|
||||
"sigmas": [
|
||||
"3",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"10",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SamplerCustomAdvanced",
|
||||
"_meta": {
|
||||
"title": "自定义采样器(高级)"
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"inputs": {
|
||||
"scheduler": "simple",
|
||||
"steps": 20,
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicScheduler",
|
||||
"_meta": {
|
||||
"title": "基本调度器"
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"inputs": {
|
||||
"sampler_name": "euler"
|
||||
},
|
||||
"class_type": "KSamplerSelect",
|
||||
"_meta": {
|
||||
"title": "K采样器选择"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"noise_seed": 1091846418466568
|
||||
},
|
||||
"class_type": "RandomNoise",
|
||||
"_meta": {
|
||||
"title": "随机噪波"
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"clip_name1": "t5xxl_fp16.safetensors",
|
||||
"clip_name2": "clip_l.safetensors",
|
||||
"type": "flux",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "DualCLIPLoader",
|
||||
"_meta": {
|
||||
"title": "双CLIP加载器"
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"model": [
|
||||
"8",
|
||||
0
|
||||
],
|
||||
"conditioning": [
|
||||
"11",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicGuider",
|
||||
"_meta": {
|
||||
"title": "基本引导器"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"unet_name": "flux1-dev.safetensors",
|
||||
"weight_dtype": "fp8_e4m3fn"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"_meta": {
|
||||
"title": "UNet加载器"
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"vae_name": "ae.safetensors"
|
||||
},
|
||||
"class_type": "VAELoader",
|
||||
"_meta": {
|
||||
"title": "加载VAE"
|
||||
}
|
||||
},
|
||||
"10": {
|
||||
"inputs": {
|
||||
"width": [
|
||||
"27",
|
||||
0
|
||||
],
|
||||
"height": [
|
||||
"28",
|
||||
0
|
||||
],
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage",
|
||||
"_meta": {
|
||||
"title": "空Latent图像"
|
||||
}
|
||||
},
|
||||
"11": {
|
||||
"inputs": {
|
||||
"text": [
|
||||
"21",
|
||||
0
|
||||
],
|
||||
"clip": [
|
||||
"6",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {
|
||||
"title": "CLIP文本编码"
|
||||
}
|
||||
},
|
||||
"14": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"2",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"9",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {
|
||||
"title": "VAE解码"
|
||||
}
|
||||
},
|
||||
"20": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"14",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveImage",
|
||||
"_meta": {
|
||||
"title": "保存图像"
|
||||
}
|
||||
},
|
||||
"21": {
|
||||
"inputs": {
|
||||
"text": "A serene and peaceful scene of a European/American girl playing guitar by the seaside. The background features a calm ocean under soft, natural lighting, with tranquil sky and subtle waves. The overall atmosphere is quiet and harmonious, evoking a sense of relaxation and gentle breeze. Horizontal composition, photorealistic style,"
|
||||
},
|
||||
"class_type": "Text _O",
|
||||
"_meta": {
|
||||
"title": "$prompt.text!:image prompt"
|
||||
}
|
||||
},
|
||||
"24": {
|
||||
"inputs": {
|
||||
"text": "Generate high-quality images from text prompts using local FLUX 1.1 model.\n \n Creates completely new images with exceptional detail, style diversity, and prompt adherence.\n The model excels at photorealistic scenes, artistic styles, and complex compositions.\n \n Prompt writing tips for best results:\n - Be detailed and specific: include colors, mood, lighting, and composition\n - Use natural language and full sentences rather than keywords\n - For photos: mention camera type, settings (e.g., \"shot on DSLR, f/2.8, golden hour\")\n - For portraits: describe features, expressions, clothing, and background\n - For scenes: specify foreground, middle ground, and background elements\n \n Excellent prompt examples:\n - \"birthday, girl selfie with bright smile and colorful balloons\"\n - \"Close-up portrait of a child with bright vivid emotions, sparkling eyes full of joy, sunlight dancing on rosy cheeks\"\n - \"Serene lake reflecting dense forest during golden hour, shot on DSLR with wide-angle lens, f/8, perfect symmetry\"\n - \"Vintage travel poster for Paris, Eiffel Tower silhouette in warm sunset colors, 'PARIS' in golden Art Deco font\"\n - \"Ethereal dragon with neon lightning crystal on head, glowing blue eyes, majestic wings spread wide\""
|
||||
},
|
||||
"class_type": "Text _O",
|
||||
"_meta": {
|
||||
"title": "MCP"
|
||||
}
|
||||
},
|
||||
"27": {
|
||||
"inputs": {
|
||||
"value": 1024
|
||||
},
|
||||
"class_type": "easy int",
|
||||
"_meta": {
|
||||
"title": "$width.value"
|
||||
}
|
||||
},
|
||||
"28": {
|
||||
"inputs": {
|
||||
"value": 1024
|
||||
},
|
||||
"class_type": "easy int",
|
||||
"_meta": {
|
||||
"title": "$height.value"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user