更新配置文件,重构工作流管理逻辑,调整国际化文件文职,优化图像生成和文本转语音服务的工作流解析,确保默认工作流配置必填,调整前端工作流选择逻辑。
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
International language support for ReelForge
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
_locales: Dict[str, dict] = {}
|
||||
_current_language: str = "zh_CN"
|
||||
|
||||
|
||||
def load_locales() -> Dict[str, dict]:
|
||||
"""Load all locale files from locales directory"""
|
||||
global _locales
|
||||
|
||||
locales_dir = Path(__file__).parent / "locales"
|
||||
|
||||
if not locales_dir.exists():
|
||||
logger.warning(f"Locales directory not found: {locales_dir}")
|
||||
return _locales
|
||||
|
||||
for json_file in locales_dir.glob("*.json"):
|
||||
lang_code = json_file.stem
|
||||
try:
|
||||
with open(json_file, "r", encoding="utf-8") as f:
|
||||
_locales[lang_code] = json.load(f)
|
||||
logger.debug(f"Loaded locale: {lang_code}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load locale {lang_code}: {e}")
|
||||
|
||||
logger.info(f"Loaded {len(_locales)} locales: {list(_locales.keys())}")
|
||||
return _locales
|
||||
|
||||
|
||||
def set_language(lang_code: str):
|
||||
"""Set current language"""
|
||||
global _current_language
|
||||
if lang_code in _locales:
|
||||
_current_language = lang_code
|
||||
logger.debug(f"Language set to: {lang_code}")
|
||||
else:
|
||||
logger.warning(f"Language {lang_code} not found, keeping {_current_language}")
|
||||
|
||||
|
||||
def get_language() -> str:
|
||||
"""Get current language"""
|
||||
return _current_language
|
||||
|
||||
|
||||
def tr(key: str, fallback: Optional[str] = None, **kwargs) -> str:
|
||||
"""
|
||||
Translate a key to current language
|
||||
|
||||
Args:
|
||||
key: Translation key (e.g., "app.title")
|
||||
fallback: Fallback text if key not found
|
||||
**kwargs: Format parameters for string interpolation
|
||||
|
||||
Returns:
|
||||
Translated text
|
||||
|
||||
Example:
|
||||
tr("app.title") # => "ReelForge"
|
||||
tr("error.missing_field", field="API Key") # => "请填写 API Key"
|
||||
"""
|
||||
locale = _locales.get(_current_language, {})
|
||||
translations = locale.get("t", {})
|
||||
|
||||
result = translations.get(key)
|
||||
|
||||
if result is None:
|
||||
# Try fallback parameter
|
||||
if fallback is not None:
|
||||
result = fallback
|
||||
# Try English fallback
|
||||
elif _current_language != "en_US" and "en_US" in _locales:
|
||||
en_locale = _locales["en_US"]
|
||||
result = en_locale.get("t", {}).get(key)
|
||||
|
||||
# Last resort: return the key itself
|
||||
if result is None:
|
||||
result = key
|
||||
logger.debug(f"Translation missing: {key}")
|
||||
|
||||
# Apply string interpolation if kwargs provided
|
||||
if kwargs:
|
||||
try:
|
||||
result = result.format(**kwargs)
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.warning(f"Failed to format translation '{key}': {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_language_name(lang_code: Optional[str] = None) -> str:
|
||||
"""Get display name of a language"""
|
||||
if lang_code is None:
|
||||
lang_code = _current_language
|
||||
|
||||
locale = _locales.get(lang_code, {})
|
||||
return locale.get("language_name", lang_code)
|
||||
|
||||
|
||||
def get_available_languages() -> Dict[str, str]:
|
||||
"""Get all available languages with their display names"""
|
||||
return {
|
||||
code: locale.get("language_name", code)
|
||||
for code, locale in _locales.items()
|
||||
}
|
||||
|
||||
|
||||
# Auto-load locales on import
|
||||
load_locales()
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
{
|
||||
"language_name": "English",
|
||||
"t": {
|
||||
"app.title": "⚡ ReelForge - AI Auto Short Video Engine",
|
||||
"app.subtitle": "Forge your perfect reel engine",
|
||||
|
||||
"section.content_input": "📖 Content Input",
|
||||
"section.audio_settings": "🔊 Audio Settings",
|
||||
"section.visual_settings": "🎨 Visual Settings",
|
||||
"section.video_generation": "🎬 Generate Video",
|
||||
|
||||
"input_mode.book": "📚 Book Name",
|
||||
"input_mode.topic": "💡 Topic",
|
||||
"input_mode.custom": "✍️ Custom Content",
|
||||
|
||||
"mode.generate": "💡 Generate Mode",
|
||||
"mode.fixed": "📄 Fixed Mode",
|
||||
|
||||
"input.book_name": "Book Name",
|
||||
"input.book_name_placeholder": "e.g., Atomic Habits, How to Win Friends",
|
||||
"input.book_name_help": "Enter the book name, will fetch book info and generate video",
|
||||
|
||||
"input.topic": "Topic",
|
||||
"input.topic_placeholder": "AI automatically creates specified number of narrations\nExample: How to build passive income, 如何增加被动收入",
|
||||
"input.topic_help": "Enter a topic, AI will generate content based on it",
|
||||
|
||||
"input.text": "Text Input",
|
||||
"input.text_help_generate": "Enter topic or theme (AI will create narrations)",
|
||||
"input.text_help_fixed": "Enter complete narration script (used directly without modification, one narration per line)",
|
||||
|
||||
"input.content": "Content",
|
||||
"input.content_placeholder": "Used directly without modification, one narration per line\nExample:\nHello everyone, today I'll share three study tips\nThe first tip is focus training, meditate for 10 minutes daily\nThe second tip is active recall, review immediately after learning",
|
||||
"input.content_help": "Provide your own content for video generation",
|
||||
|
||||
"input.title": "Title (Optional)",
|
||||
"input.title_placeholder": "Video title (auto-generated if empty)",
|
||||
"input.title_help": "Optional: Custom title for the video",
|
||||
|
||||
"book.search": "🔍 Search Book",
|
||||
"book.searching": "Searching book...",
|
||||
"book.found": "✅ Book found!",
|
||||
"book.not_found": "❌ Failed to fetch book: {error}",
|
||||
"book.name_required": "Please enter a book name",
|
||||
|
||||
"book.title": "Title",
|
||||
"book.author": "Author",
|
||||
"book.rating": "Rating",
|
||||
"book.summary": "📝 Summary",
|
||||
|
||||
"voice.title": "🎤 Voice Selection",
|
||||
"voice.male_professional": "🎤 Male-Professional",
|
||||
"voice.male_young": "🎙️ Male-Young",
|
||||
"voice.female_gentle": "🎵 Female-Gentle",
|
||||
"voice.female_energetic": "🎶 Female-Energetic",
|
||||
"voice.preview": "▶ Preview Voice",
|
||||
"voice.previewing": "Generating voice preview...",
|
||||
"voice.preview_failed": "Preview failed: {error}",
|
||||
|
||||
"style.title": "🎨 Image Settings",
|
||||
"style.workflow": "ComfyUI Workflow",
|
||||
"style.workflow_help": "💡 Custom: Place image_xxx.json in workflows/ folder",
|
||||
"style.prompt_prefix": "Style Prompt Prefix",
|
||||
"style.prompt_prefix_placeholder": "Enter style prefix (leave empty for config default)",
|
||||
"style.prompt_prefix_help": "This text will be automatically added before all image generation prompts. To permanently change, edit config.yaml",
|
||||
"style.custom": "Custom",
|
||||
"style.description": "Style Description",
|
||||
"style.description_placeholder": "Describe the illustration style you want (any language)...",
|
||||
"style.preview": "🖼️ Preview Style",
|
||||
"style.previewing": "Generating style preview...",
|
||||
"style.preview_caption": "Style Preview",
|
||||
"style.preview_failed": "Preview failed: {error}",
|
||||
"style.generated_prompt": "Generated prompt: {prompt}",
|
||||
|
||||
"template.title": "📐 Storyboard Template",
|
||||
"template.default": "Default",
|
||||
"template.modern": "Modern",
|
||||
"template.neon": "Neon",
|
||||
"template.custom_help": "💡 Custom: Place .html files in templates/ folder",
|
||||
"template.preview_title": "🔍 Preview Template",
|
||||
"template.preview_param_title": "Title",
|
||||
"template.preview_param_text": "Text",
|
||||
"template.preview_param_image": "Image Path",
|
||||
"template.preview_param_width": "Width",
|
||||
"template.preview_param_height": "Height",
|
||||
"template.preview_default_title": "AI Changes Content Creation",
|
||||
"template.preview_default_text": "Artificial intelligence is transforming the way we create content, making it easy for everyone to produce professional-grade videos.",
|
||||
"template.preview_button": "🖼️ Generate Preview",
|
||||
"template.preview_generating": "Generating template preview...",
|
||||
"template.preview_success": "✅ Preview generated successfully!",
|
||||
"template.preview_failed": "❌ Preview failed: {error}",
|
||||
"template.preview_image_help": "Supports local path or URL",
|
||||
"template.preview_caption": "Template Preview: {template}",
|
||||
|
||||
"video.title": "🎬 Video Settings",
|
||||
"video.frames": "Scenes",
|
||||
"video.frames_help": "More scenes = longer video",
|
||||
"video.frames_label": "Scenes: {n}",
|
||||
"video.frames_fixed_mode_hint": "💡 Fixed mode: scene count is determined by actual script segments",
|
||||
|
||||
"bgm.title": "🎵 Background Music",
|
||||
"bgm.none": "🔇 No BGM",
|
||||
"bgm.preview": "▶ Preview Music",
|
||||
"bgm.preview_failed": "❌ Music file not found: {file}",
|
||||
"bgm.custom_help": "💡 Custom: Place audio files in bgm/ folder",
|
||||
|
||||
"btn.generate": "🎬 Generate Video",
|
||||
"btn.save_config": "💾 Save Configuration",
|
||||
"btn.reset_config": "🔄 Reset to Default",
|
||||
"btn.save_and_start": "Save and Start",
|
||||
"btn.test_connection": "Test Connection",
|
||||
|
||||
"status.initializing": "🔧 Initializing...",
|
||||
"status.generating": "🚀 Generating video...",
|
||||
"status.success": "✅ Video generated successfully!",
|
||||
"status.error": "❌ Generation failed: {error}",
|
||||
"status.video_generated": "✅ Video generated: {path}",
|
||||
"status.video_not_found": "Video file not found: {path}",
|
||||
"status.config_saved": "✅ Configuration saved",
|
||||
"status.config_reset": "✅ Configuration reset to default",
|
||||
"status.connection_success": "✅ Connected",
|
||||
"status.connection_failed": "❌ Connection failed",
|
||||
|
||||
"progress.generating_narrations": "Generating narrations...",
|
||||
"progress.splitting_script": "Splitting script...",
|
||||
"progress.generating_image_prompts": "Generating image prompts...",
|
||||
"progress.frame": "Frame {current}/{total}",
|
||||
"progress.frame_step": "Frame {current}/{total} - Step {step}/4: {action}",
|
||||
"progress.step_audio": "Generating audio",
|
||||
"progress.step_image": "Generating image",
|
||||
"progress.step_compose": "Composing frame",
|
||||
"progress.step_video": "Creating video segment",
|
||||
"progress.concatenating": "Concatenating video segments...",
|
||||
"progress.finalizing": "Finalizing...",
|
||||
|
||||
"error.input_required": "❌ Please provide book name, topic, or content",
|
||||
"error.api_key_required": "❌ Please enter API Key",
|
||||
"error.missing_field": "Please enter {field}",
|
||||
|
||||
"info.duration": "Duration",
|
||||
"info.file_size": "File Size",
|
||||
"info.frames": "Scenes",
|
||||
"info.scenes_unit": " scenes",
|
||||
"info.resolution": "Resolution",
|
||||
"info.video_information": "📊 Video Information",
|
||||
"info.no_video_yet": "Video preview will appear here after generation",
|
||||
|
||||
"settings.title": "⚙️ System Configuration (Required)",
|
||||
"settings.not_configured": "⚠️ Please complete system configuration before generating videos",
|
||||
"settings.llm.title": "🤖 Large Language Model",
|
||||
"settings.llm.quick_select": "Quick Select",
|
||||
"settings.llm.quick_select_help": "Choose a preset LLM or custom configuration",
|
||||
"settings.llm.get_api_key": "Get API Key",
|
||||
"settings.llm.api_key": "API Key",
|
||||
"settings.llm.api_key_help": "Enter your API Key",
|
||||
"settings.llm.base_url": "Base URL",
|
||||
"settings.llm.base_url_help": "API service address",
|
||||
"settings.llm.model": "Model",
|
||||
"settings.llm.model_help": "Model name",
|
||||
|
||||
"settings.tts.title": "🎤 Text-to-Speech",
|
||||
"settings.tts.provider": "Provider",
|
||||
"settings.tts.provider_help": "Select TTS service provider",
|
||||
"settings.tts.edge_info": "💡 Edge TTS is free and requires no configuration",
|
||||
|
||||
"settings.image.title": "🎨 Image Generation",
|
||||
"settings.image.local_title": "Local/Self-hosted ComfyUI",
|
||||
"settings.image.cloud_title": "RunningHub Cloud",
|
||||
"settings.image.comfyui_url": "ComfyUI Service URL",
|
||||
"settings.image.comfyui_url_help": "Local or remote ComfyUI service URL, default: http://127.0.0.1:8188",
|
||||
"settings.image.runninghub_api_key": "RunningHub API Key",
|
||||
"settings.image.runninghub_api_key_help": "Visit https://runninghub.ai to register and get API Key",
|
||||
|
||||
"settings.book.title": "📚 Book Information",
|
||||
"settings.book.provider": "Provider",
|
||||
"settings.book.provider_help": "Select book information source",
|
||||
|
||||
"welcome.first_time": "🎉 Welcome to ReelForge! Please complete basic configuration",
|
||||
"welcome.config_hint": "💡 First-time setup requires API Key configuration, you can modify it in advanced settings later",
|
||||
|
||||
"wizard.llm_required": "🤖 Large Language Model Configuration (Required)",
|
||||
"wizard.image_optional": "🎨 Image Generation Configuration (Optional)",
|
||||
"wizard.image_hint": "💡 If not configured, default template will be used (no AI image generation)",
|
||||
"wizard.configure_image": "Configure Image Generation (Recommended)",
|
||||
|
||||
"label.required": "(Required)",
|
||||
"label.optional": "(Optional)",
|
||||
|
||||
"language.select": "🌐 Language"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
{
|
||||
"language_name": "简体中文",
|
||||
"t": {
|
||||
"app.title": "⚡ ReelForge - AI 全自动短视频引擎",
|
||||
"app.subtitle": "打造专属你的视频创作引擎",
|
||||
|
||||
"section.content_input": "📖 内容输入",
|
||||
"section.audio_settings": "🔊 声音设置",
|
||||
"section.visual_settings": "🎨 画面设置",
|
||||
"section.video_generation": "🎬 生成视频",
|
||||
|
||||
"input_mode.book": "📚 书名",
|
||||
"input_mode.topic": "💡 主题",
|
||||
"input_mode.custom": "✍️ 自定义内容",
|
||||
|
||||
"mode.generate": "💡 生成模式",
|
||||
"mode.fixed": "📄 固定模式",
|
||||
|
||||
"input.book_name": "书名",
|
||||
"input.book_name_placeholder": "例如:原子习惯、人性的弱点、Atomic Habits",
|
||||
"input.book_name_help": "输入书名,将自动获取书籍信息并生成视频",
|
||||
|
||||
"input.topic": "主题",
|
||||
"input.topic_placeholder": "AI 自动创作指定数量的旁白\n例如:如何增加被动收入、How to build passive income",
|
||||
"input.topic_help": "输入一个主题,AI 将根据主题生成内容",
|
||||
|
||||
"input.text": "文本输入",
|
||||
"input.text_help_generate": "输入主题或话题(AI 将创作旁白)",
|
||||
"input.text_help_fixed": "输入完整的旁白脚本(直接使用,不做改写,每行一个旁白)",
|
||||
|
||||
"input.content": "内容",
|
||||
"input.content_placeholder": "直接使用,不做改写,每行一个旁白\n例如:\n大家好,今天跟你分享三个学习技巧\n第一个技巧是专注力训练,每天冥想10分钟\n第二个技巧是主动回忆,学完立即复述",
|
||||
"input.content_help": "提供您自己的内容用于视频生成",
|
||||
|
||||
"input.title": "标题(可选)",
|
||||
"input.title_placeholder": "视频标题(留空则自动生成)",
|
||||
"input.title_help": "可选:自定义视频标题",
|
||||
|
||||
"book.search": "🔍 搜索书籍",
|
||||
"book.searching": "正在搜索书籍...",
|
||||
"book.found": "✅ 找到书籍!",
|
||||
"book.not_found": "❌ 获取书籍失败:{error}",
|
||||
"book.name_required": "请输入书名",
|
||||
|
||||
"book.title": "书名",
|
||||
"book.author": "作者",
|
||||
"book.rating": "评分",
|
||||
"book.summary": "📝 简介",
|
||||
|
||||
"voice.title": "🎤 语音选择",
|
||||
"voice.male_professional": "🎤 男声-专业",
|
||||
"voice.male_young": "🎙️ 男声-年轻",
|
||||
"voice.female_gentle": "🎵 女声-温柔",
|
||||
"voice.female_energetic": "🎶 女声-活力",
|
||||
"voice.preview": "▶ 试听语音",
|
||||
"voice.previewing": "正在生成语音预览...",
|
||||
"voice.preview_failed": "预览失败:{error}",
|
||||
|
||||
"style.title": "🎨 插图设置",
|
||||
"style.workflow": "生图工作流",
|
||||
"style.workflow_help": "💡 自定义:将 image_xxx.json 放入 workflows/ 文件夹",
|
||||
"style.prompt_prefix": "风格提示词前缀",
|
||||
"style.prompt_prefix_placeholder": "输入风格前缀(留空则使用配置文件默认值)",
|
||||
"style.prompt_prefix_help": "此文本将自动添加到所有图像生成提示词之前。要永久修改,请编辑 config.yaml",
|
||||
"style.custom": "自定义",
|
||||
"style.description": "风格描述",
|
||||
"style.description_placeholder": "描述您想要的插图风格(任何语言)...",
|
||||
"style.preview": "🖼️ 预览风格",
|
||||
"style.previewing": "正在生成风格预览...",
|
||||
"style.preview_caption": "风格预览",
|
||||
"style.preview_failed": "预览失败:{error}",
|
||||
"style.generated_prompt": "生成的提示词:{prompt}",
|
||||
|
||||
"template.title": "📐 分镜模板",
|
||||
"template.default": "默认",
|
||||
"template.modern": "现代",
|
||||
"template.neon": "霓虹",
|
||||
"template.custom_help": "💡 自定义:将 .html 文件放入 templates/ 文件夹",
|
||||
"template.preview_title": "🔍 预览模板",
|
||||
"template.preview_param_title": "标题",
|
||||
"template.preview_param_text": "文本",
|
||||
"template.preview_param_image": "图片路径",
|
||||
"template.preview_param_width": "宽度",
|
||||
"template.preview_param_height": "高度",
|
||||
"template.preview_default_title": "AI 改变内容创作",
|
||||
"template.preview_default_text": "人工智能正在改变内容创作的方式,让每个人都能轻松制作专业级视频。",
|
||||
"template.preview_button": "🖼️ 生成预览",
|
||||
"template.preview_generating": "正在生成模板预览...",
|
||||
"template.preview_success": "✅ 预览生成成功!",
|
||||
"template.preview_failed": "❌ 预览失败:{error}",
|
||||
"template.preview_image_help": "支持本地路径或 URL",
|
||||
"template.preview_caption": "模板预览:{template}",
|
||||
|
||||
"video.title": "🎬 视频设置",
|
||||
"video.frames": "分镜数",
|
||||
"video.frames_help": "更多分镜 = 更长视频",
|
||||
"video.frames_label": "分镜数:{n}",
|
||||
"video.frames_fixed_mode_hint": "💡 固定模式:分镜数由脚本实际段落数决定",
|
||||
|
||||
"bgm.title": "🎵 背景音乐",
|
||||
"bgm.none": "🔇 无背景音乐",
|
||||
"bgm.preview": "▶ 试听音乐",
|
||||
"bgm.preview_failed": "❌ 音乐文件未找到:{file}",
|
||||
"bgm.custom_help": "💡 自定义:将音频文件放入 bgm/ 文件夹",
|
||||
|
||||
"btn.generate": "🎬 生成视频",
|
||||
"btn.save_config": "💾 保存配置",
|
||||
"btn.reset_config": "🔄 重置默认",
|
||||
"btn.save_and_start": "保存并开始",
|
||||
"btn.test_connection": "测试连接",
|
||||
|
||||
"status.initializing": "🔧 正在初始化...",
|
||||
"status.generating": "🚀 正在生成视频...",
|
||||
"status.success": "✅ 视频生成成功!",
|
||||
"status.error": "❌ 生成失败:{error}",
|
||||
"status.video_generated": "✅ 视频已生成:{path}",
|
||||
"status.video_not_found": "视频文件未找到:{path}",
|
||||
"status.config_saved": "✅ 配置已保存",
|
||||
"status.config_reset": "✅ 配置已重置为默认值",
|
||||
"status.connection_success": "✅ 连接成功",
|
||||
"status.connection_failed": "❌ 连接失败",
|
||||
|
||||
"progress.generating_narrations": "生成旁白...",
|
||||
"progress.splitting_script": "切分脚本...",
|
||||
"progress.generating_image_prompts": "生成图片提示词...",
|
||||
"progress.frame": "分镜 {current}/{total}",
|
||||
"progress.frame_step": "分镜 {current}/{total} - 步骤 {step}/4: {action}",
|
||||
"progress.step_audio": "生成语音",
|
||||
"progress.step_image": "生成插图",
|
||||
"progress.step_compose": "合成画面",
|
||||
"progress.step_video": "创建视频片段",
|
||||
"progress.concatenating": "拼接视频片段...",
|
||||
"progress.finalizing": "完成中...",
|
||||
|
||||
"error.input_required": "❌ 请提供书名、主题或内容",
|
||||
"error.api_key_required": "❌ 请填写 API Key",
|
||||
"error.missing_field": "请填写 {field}",
|
||||
|
||||
"info.duration": "时长",
|
||||
"info.file_size": "文件大小",
|
||||
"info.frames": "分镜数",
|
||||
"info.scenes_unit": "分镜",
|
||||
"info.resolution": "分辨率",
|
||||
"info.video_information": "📊 视频信息",
|
||||
"info.no_video_yet": "生成视频后,预览将显示在这里",
|
||||
|
||||
"settings.title": "⚙️ 系统配置(必需)",
|
||||
"settings.not_configured": "⚠️ 请先完成系统配置才能生成视频",
|
||||
"settings.llm.title": "🤖 大语言模型",
|
||||
"settings.llm.quick_select": "快速选择",
|
||||
"settings.llm.quick_select_help": "选择预置的 LLM 或自定义配置",
|
||||
"settings.llm.get_api_key": "获取 API Key",
|
||||
"settings.llm.api_key": "API Key",
|
||||
"settings.llm.api_key_help": "填入您的 API Key",
|
||||
"settings.llm.base_url": "Base URL",
|
||||
"settings.llm.base_url_help": "API 服务地址",
|
||||
"settings.llm.model": "Model",
|
||||
"settings.llm.model_help": "模型名称",
|
||||
|
||||
"settings.tts.title": "🎤 语音合成",
|
||||
"settings.tts.provider": "服务商",
|
||||
"settings.tts.provider_help": "选择 TTS 服务提供商",
|
||||
"settings.tts.edge_info": "💡 Edge TTS 是免费的,无需配置",
|
||||
|
||||
"settings.image.title": "🎨 图像生成",
|
||||
"settings.image.local_title": "本地/自建 ComfyUI",
|
||||
"settings.image.cloud_title": "RunningHub 云端",
|
||||
"settings.image.comfyui_url": "ComfyUI 服务地址",
|
||||
"settings.image.comfyui_url_help": "本地或远程 ComfyUI 服务地址,默认: http://127.0.0.1:8188",
|
||||
"settings.image.runninghub_api_key": "RunningHub API Key",
|
||||
"settings.image.runninghub_api_key_help": "访问 https://runninghub.ai 注册并获取 API Key",
|
||||
|
||||
"settings.book.title": "📚 书籍信息",
|
||||
"settings.book.provider": "服务商",
|
||||
"settings.book.provider_help": "选择书籍信息来源",
|
||||
|
||||
"welcome.first_time": "🎉 欢迎使用 ReelForge!请先完成基础配置",
|
||||
"welcome.config_hint": "💡 首次使用需要配置 API Key,后续可以在高级设置中修改",
|
||||
|
||||
"wizard.llm_required": "🤖 大语言模型配置(必需)",
|
||||
"wizard.image_optional": "🎨 图像生成配置(可选)",
|
||||
"wizard.image_hint": "💡 如果不配置图像生成,将使用默认模板(无 AI 生图)",
|
||||
"wizard.configure_image": "配置图像生成(推荐)",
|
||||
|
||||
"label.required": "(必需)",
|
||||
"label.optional": "(可选)",
|
||||
|
||||
"language.select": "🌐 语言"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user