重构配置逻辑
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,6 +41,8 @@ Thumbs.db
|
||||
|
||||
# Config files with sensitive data
|
||||
config.yaml
|
||||
config.yaml.bak
|
||||
*.yaml.bak
|
||||
.env
|
||||
|
||||
# Logs
|
||||
|
||||
@@ -1,108 +1,29 @@
|
||||
# ReelForge Configuration Example
|
||||
# 复制此文件为 config.yaml 并填入你的配置
|
||||
# ReelForge Configuration
|
||||
# Copy this file to config.yaml and fill in your settings
|
||||
# ⚠️ Never commit config.yaml to Git!
|
||||
|
||||
# Project name
|
||||
project_name: ReelForge
|
||||
|
||||
# ==================== LLM Configuration ====================
|
||||
# Simple 3-field configuration - works with any OpenAI SDK compatible LLM
|
||||
#
|
||||
# Popular choices (copy one of these):
|
||||
#
|
||||
# Qwen Max (推荐中文):
|
||||
# api_key: "sk-xxx"
|
||||
# base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
# model: "qwen-max"
|
||||
# # Get API key: https://dashscope.console.aliyun.com/apiKey
|
||||
#
|
||||
# OpenAI GPT-4o:
|
||||
# api_key: "sk-xxx"
|
||||
# base_url: "https://api.openai.com/v1"
|
||||
# model: "gpt-4o"
|
||||
# # Get API key: https://platform.openai.com/api-keys
|
||||
#
|
||||
# Claude Sonnet 4:
|
||||
# api_key: "sk-ant-xxx"
|
||||
# base_url: "https://api.anthropic.com/v1/"
|
||||
# model: "claude-sonnet-4-5"
|
||||
# # Get API key: https://console.anthropic.com/settings/keys
|
||||
#
|
||||
# DeepSeek (超高性价比):
|
||||
# api_key: "sk-xxx"
|
||||
# base_url: "https://api.deepseek.com"
|
||||
# model: "deepseek-chat"
|
||||
# # Get API key: https://platform.deepseek.com/api_keys
|
||||
#
|
||||
# Ollama (本地免费):
|
||||
# api_key: "ollama" # Any value works
|
||||
# base_url: "http://localhost:11434/v1"
|
||||
# model: "llama3.2"
|
||||
# # Install: https://ollama.com/download
|
||||
# # Then: ollama pull llama3.2
|
||||
#
|
||||
# Supports any OpenAI SDK compatible API
|
||||
llm:
|
||||
api_key: "" # Fill in your API key
|
||||
base_url: "" # LLM API endpoint
|
||||
model: "" # Model name
|
||||
api_key: ""
|
||||
base_url: ""
|
||||
model: ""
|
||||
|
||||
# Popular presets:
|
||||
# Qwen Max: base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1" model: "qwen-max"
|
||||
# OpenAI GPT-4o: base_url: "https://api.openai.com/v1" model: "gpt-4o"
|
||||
# DeepSeek: base_url: "https://api.deepseek.com" model: "deepseek-chat"
|
||||
# Ollama (Local): base_url: "http://localhost:11434/v1" model: "llama3.2"
|
||||
|
||||
# ==================== TTS Configuration ====================
|
||||
# TTS supports two modes:
|
||||
# 1. Edge TTS (default) - Free local SDK, no setup needed
|
||||
# 2. ComfyUI Workflow - Workflow-based, requires ComfyUI
|
||||
#
|
||||
# Configuration (optional):
|
||||
tts:
|
||||
default_workflow: "edge" # Default: "edge" (Edge TTS) or "tts_default.json" (ComfyUI workflow)
|
||||
# comfyui_url: http://127.0.0.1:8188 # Only needed for ComfyUI workflows
|
||||
|
||||
# Usage in code:
|
||||
# await reelforge.tts(text="hello") # Uses default (edge)
|
||||
# await reelforge.tts(text="hello", workflow="edge") # Explicitly use Edge TTS
|
||||
# await reelforge.tts(text="hello", workflow="tts_custom.json") # Use ComfyUI workflow
|
||||
default: edge # "edge" (free) or "tts_xxx.json" (ComfyUI workflow)
|
||||
|
||||
# ==================== Image Generation Configuration ====================
|
||||
# Image generation uses ComfyUI workflows
|
||||
# Workflows are auto-discovered from workflows/image_*.json files
|
||||
image:
|
||||
default: default # Default preset name (uses workflows/image_default.json)
|
||||
comfyui_url: http://127.0.0.1:8188 # Local ComfyUI server
|
||||
# runninghub_api_key: "" # Optional: RunningHub cloud API key
|
||||
|
||||
# Prompt prefix - automatically added to all image prompts
|
||||
# Leave empty ("") if you don't want any prefix
|
||||
default: image_default.json
|
||||
comfyui_url: http://127.0.0.1:8188
|
||||
runninghub_api_key: ""
|
||||
prompt_prefix: "Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines"
|
||||
|
||||
# Common examples:
|
||||
# prompt_prefix: "" # No prefix
|
||||
# prompt_prefix: "anime style, vibrant colors, cel shading" # Anime style
|
||||
# prompt_prefix: "watercolor painting, soft edges, artistic" # Watercolor
|
||||
# prompt_prefix: "photorealistic, 8k, professional photography" # Realistic
|
||||
|
||||
# ==================== Notes ====================
|
||||
# 1. LLM Configuration:
|
||||
# - Simple 3-field config: api_key, base_url, model
|
||||
# - Works with ANY OpenAI SDK compatible LLM
|
||||
# - Popular choices listed in comments above
|
||||
# - Switch LLM: just copy-paste different values from comments
|
||||
# - WebUI provides quick preset selection
|
||||
#
|
||||
# 2. TTS Configuration:
|
||||
# - Two modes: Edge TTS (default, free) or ComfyUI Workflow
|
||||
# - Edge TTS: No setup needed, just use default
|
||||
# - ComfyUI: Create workflow files in workflows/tts_*.json
|
||||
# - Override in code: await reelforge.tts(text="...", workflow="edge" or "tts_xxx.json")
|
||||
#
|
||||
# 3. Image Generation:
|
||||
# - Add workflow files: workflows/image_*.json
|
||||
# - Auto-discovered presets: workflows/image_flux.json -> preset="flux"
|
||||
# - Default preset: workflows/image_default.json
|
||||
#
|
||||
# 4. Ollama (Recommended for Privacy):
|
||||
# - FREE: No API costs
|
||||
# - PRIVATE: Data never leaves your machine
|
||||
# - Install: https://ollama.com/download
|
||||
# - Usage: ollama pull llama3.2
|
||||
#
|
||||
# 5. Security:
|
||||
# - Never commit config.yaml to version control
|
||||
# - All sensitive data (API keys) should stay local
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
ReelForge - AI-powered video generator with pluggable capabilities
|
||||
ReelForge - AI-powered video generator
|
||||
|
||||
Convention-based capability system using FastMCP and LiteLLM.
|
||||
Convention-based system with unified configuration management.
|
||||
|
||||
Usage:
|
||||
from reelforge import reelforge
|
||||
@@ -18,8 +18,9 @@ Usage:
|
||||
"""
|
||||
|
||||
from reelforge.service import ReelForgeCore, reelforge
|
||||
from reelforge.config import config_manager
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = ["ReelForgeCore", "reelforge"]
|
||||
__all__ = ["ReelForgeCore", "reelforge", "config_manager"]
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
"""
|
||||
Configuration system for ReelForge
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def load_config(config_path: str = "config.yaml") -> dict[str, Any]:
|
||||
"""
|
||||
Load configuration from YAML file
|
||||
|
||||
User-friendly flat format (no internal conversion):
|
||||
project_name: ReelForge
|
||||
llm:
|
||||
api_key: xxx
|
||||
base_url: xxx
|
||||
model: xxx
|
||||
tts:
|
||||
default: edge
|
||||
edge: null
|
||||
image:
|
||||
default: comfykit
|
||||
comfykit:
|
||||
comfyui_url: http://xxx
|
||||
|
||||
Args:
|
||||
config_path: Path to config file (default: config.yaml)
|
||||
|
||||
Returns:
|
||||
Configuration dict (as-is from YAML)
|
||||
"""
|
||||
# Check if config file exists
|
||||
config_file = Path(config_path)
|
||||
if not config_file.exists():
|
||||
logger.warning(f"Config file not found: {config_path}")
|
||||
logger.info("Using default configuration")
|
||||
return _get_default_config()
|
||||
|
||||
# Load config
|
||||
logger.info(f"Loading config from: {config_path}")
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Handle None (empty YAML file)
|
||||
if config is None:
|
||||
config = {}
|
||||
|
||||
# Ensure project_name exists
|
||||
if "project_name" not in config:
|
||||
config["project_name"] = "ReelForge"
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _get_default_config() -> dict[str, Any]:
|
||||
"""Get default configuration"""
|
||||
return {
|
||||
"project_name": "ReelForge",
|
||||
"llm": {
|
||||
"api_key": "",
|
||||
"base_url": "",
|
||||
"model": ""
|
||||
},
|
||||
"tts": {
|
||||
"default": "edge",
|
||||
"edge": None
|
||||
},
|
||||
"image": {
|
||||
"default": "comfykit",
|
||||
"comfykit": {
|
||||
"comfyui_url": "http://127.0.0.1:8188"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
reelforge/config/__init__.py
Normal file
37
reelforge/config/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
ReelForge Configuration System
|
||||
|
||||
Unified configuration management with Pydantic validation.
|
||||
|
||||
Usage:
|
||||
from reelforge.config import config_manager
|
||||
|
||||
# Access config (type-safe)
|
||||
api_key = config_manager.config.llm.api_key
|
||||
|
||||
# Update config
|
||||
config_manager.update({"llm": {"api_key": "xxx"}})
|
||||
config_manager.save()
|
||||
|
||||
# Validate
|
||||
if config_manager.validate():
|
||||
print("Config is valid!")
|
||||
"""
|
||||
from .schema import ReelForgeConfig, LLMConfig, TTSConfig, ImageConfig
|
||||
from .manager import ConfigManager
|
||||
from .loader import load_config_dict, save_config_dict
|
||||
|
||||
# Global singleton instance
|
||||
config_manager = ConfigManager()
|
||||
|
||||
__all__ = [
|
||||
"ReelForgeConfig",
|
||||
"LLMConfig",
|
||||
"TTSConfig",
|
||||
"ImageConfig",
|
||||
"ConfigManager",
|
||||
"config_manager",
|
||||
"load_config_dict",
|
||||
"save_config_dict",
|
||||
]
|
||||
|
||||
53
reelforge/config/loader.py
Normal file
53
reelforge/config/loader.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Configuration loader - Pure YAML
|
||||
|
||||
Handles loading and saving configuration from/to YAML files.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def load_config_dict(config_path: str = "config.yaml") -> dict:
|
||||
"""
|
||||
Load configuration from YAML file
|
||||
|
||||
Args:
|
||||
config_path: Path to config file
|
||||
|
||||
Returns:
|
||||
Configuration dictionary
|
||||
"""
|
||||
config_file = Path(config_path)
|
||||
|
||||
if not config_file.exists():
|
||||
logger.warning(f"Config file not found: {config_path}")
|
||||
logger.info("Using default configuration")
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
logger.info(f"Configuration loaded from {config_path}")
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def save_config_dict(config: dict, config_path: str = "config.yaml"):
|
||||
"""
|
||||
Save configuration to YAML file
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
config_path: Path to config file
|
||||
"""
|
||||
try:
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
logger.info(f"Configuration saved to {config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
raise
|
||||
|
||||
119
reelforge/config/manager.py
Normal file
119
reelforge/config/manager.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Configuration Manager - Singleton pattern
|
||||
|
||||
Provides unified access to configuration with automatic validation.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from loguru import logger
|
||||
from .schema import ReelForgeConfig
|
||||
from .loader import load_config_dict, save_config_dict
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""
|
||||
Configuration Manager (Singleton)
|
||||
|
||||
Provides unified access to configuration with automatic validation.
|
||||
"""
|
||||
_instance: Optional['ConfigManager'] = None
|
||||
|
||||
def __new__(cls, config_path: str = "config.yaml"):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config_path: str = "config.yaml"):
|
||||
# Only initialize once
|
||||
if hasattr(self, '_initialized'):
|
||||
return
|
||||
|
||||
self.config_path = Path(config_path)
|
||||
self.config: ReelForgeConfig = self._load()
|
||||
self._initialized = True
|
||||
|
||||
def _load(self) -> ReelForgeConfig:
|
||||
"""Load configuration from file"""
|
||||
data = load_config_dict(str(self.config_path))
|
||||
return ReelForgeConfig(**data)
|
||||
|
||||
def reload(self):
|
||||
"""Reload configuration from file"""
|
||||
self.config = self._load()
|
||||
logger.info("Configuration reloaded")
|
||||
|
||||
def save(self):
|
||||
"""Save current configuration to file"""
|
||||
save_config_dict(self.config.to_dict(), str(self.config_path))
|
||||
|
||||
def update(self, updates: dict):
|
||||
"""
|
||||
Update configuration with new values
|
||||
|
||||
Args:
|
||||
updates: Dictionary of updates (e.g., {"llm": {"api_key": "xxx"}})
|
||||
"""
|
||||
current = self.config.to_dict()
|
||||
|
||||
# Deep merge
|
||||
def deep_merge(base: dict, updates: dict) -> dict:
|
||||
for key, value in updates.items():
|
||||
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
||||
deep_merge(base[key], value)
|
||||
else:
|
||||
base[key] = value
|
||||
return base
|
||||
|
||||
merged = deep_merge(current, updates)
|
||||
self.config = ReelForgeConfig(**merged)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Dict-like access (for backward compatibility)"""
|
||||
return self.config.to_dict().get(key, default)
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Validate configuration completeness"""
|
||||
return self.config.validate_required()
|
||||
|
||||
def get_llm_config(self) -> dict:
|
||||
"""Get LLM configuration as dict"""
|
||||
return {
|
||||
"api_key": self.config.llm.api_key,
|
||||
"base_url": self.config.llm.base_url,
|
||||
"model": self.config.llm.model,
|
||||
}
|
||||
|
||||
def set_llm_config(self, api_key: str, base_url: str, model: str):
|
||||
"""Set LLM configuration"""
|
||||
self.update({
|
||||
"llm": {
|
||||
"api_key": api_key,
|
||||
"base_url": base_url,
|
||||
"model": model,
|
||||
}
|
||||
})
|
||||
|
||||
def get_image_config(self) -> dict:
|
||||
"""Get image configuration as dict"""
|
||||
return {
|
||||
"default": self.config.image.default,
|
||||
"comfyui_url": self.config.image.comfyui_url,
|
||||
"runninghub_api_key": self.config.image.runninghub_api_key,
|
||||
"prompt_prefix": self.config.image.prompt_prefix,
|
||||
}
|
||||
|
||||
def set_image_config(
|
||||
self,
|
||||
comfyui_url: Optional[str] = None,
|
||||
runninghub_api_key: Optional[str] = None
|
||||
):
|
||||
"""Set image configuration"""
|
||||
updates = {}
|
||||
if comfyui_url is not None:
|
||||
updates["comfyui_url"] = comfyui_url
|
||||
if runninghub_api_key is not None:
|
||||
updates["runninghub_api_key"] = runninghub_api_key
|
||||
|
||||
if updates:
|
||||
self.update({"image": updates})
|
||||
|
||||
54
reelforge/config/schema.py
Normal file
54
reelforge/config/schema.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Configuration schema with Pydantic models
|
||||
|
||||
Single source of truth for all configuration defaults and validation.
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LLMConfig(BaseModel):
|
||||
"""LLM configuration"""
|
||||
api_key: str = Field(default="", description="LLM API Key")
|
||||
base_url: str = Field(default="", description="LLM API Base URL")
|
||||
model: str = Field(default="", description="LLM Model Name")
|
||||
|
||||
|
||||
class TTSConfig(BaseModel):
|
||||
"""TTS configuration"""
|
||||
default: str = Field(default="edge", description="Default TTS workflow")
|
||||
|
||||
|
||||
class ImageConfig(BaseModel):
|
||||
"""Image generation configuration"""
|
||||
default: str = Field(default="image_default.json", description="Default image workflow")
|
||||
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(
|
||||
default="Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines",
|
||||
description="Prompt prefix for all image generation"
|
||||
)
|
||||
|
||||
|
||||
class ReelForgeConfig(BaseModel):
|
||||
"""ReelForge main configuration"""
|
||||
project_name: str = Field(default="ReelForge", description="Project name")
|
||||
llm: LLMConfig = Field(default_factory=LLMConfig)
|
||||
tts: TTSConfig = Field(default_factory=TTSConfig)
|
||||
image: ImageConfig = Field(default_factory=ImageConfig)
|
||||
|
||||
def is_llm_configured(self) -> bool:
|
||||
"""Check if LLM is properly configured"""
|
||||
return bool(
|
||||
self.llm.api_key and self.llm.api_key.strip() and
|
||||
self.llm.base_url and self.llm.base_url.strip() and
|
||||
self.llm.model and self.llm.model.strip()
|
||||
)
|
||||
|
||||
def validate_required(self) -> bool:
|
||||
"""Validate required configuration"""
|
||||
return self.is_llm_configured()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary (for backward compatibility)"""
|
||||
return self.model_dump()
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
"""
|
||||
Configuration manager for WebUI
|
||||
|
||||
Handles loading, saving, and validating configuration
|
||||
from the web interface.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
import yaml
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class ConfigStatus:
|
||||
"""Configuration validation status"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_complete: bool = True
|
||||
self.missing_fields: List[str] = []
|
||||
self.warnings: List[str] = []
|
||||
|
||||
def add_missing(self, field: str):
|
||||
"""Add a missing required field"""
|
||||
self.is_complete = False
|
||||
self.missing_fields.append(field)
|
||||
|
||||
def add_warning(self, message: str):
|
||||
"""Add a warning message"""
|
||||
self.warnings.append(message)
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Manage configuration for WebUI"""
|
||||
|
||||
def __init__(self, config_path: str = "config.yaml"):
|
||||
self.config_path = Path(config_path)
|
||||
self.config: Optional[Dict[str, Any]] = None
|
||||
|
||||
def load_or_create_default(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Load config from file, or create default if not exists
|
||||
|
||||
Returns:
|
||||
Configuration dictionary
|
||||
"""
|
||||
if not self.config_path.exists():
|
||||
logger.warning("Config file not found, creating default")
|
||||
self.config = self._create_default_config()
|
||||
self.save()
|
||||
else:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
logger.info(f"Configuration loaded from {self.config_path}")
|
||||
return self.config
|
||||
|
||||
def save(self):
|
||||
"""Save config to file"""
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(self.config, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
logger.info(f"Configuration saved to {self.config_path}")
|
||||
|
||||
def update_from_ui(self, ui_values: Dict[str, Any]):
|
||||
"""
|
||||
Update config from UI form values
|
||||
|
||||
Args:
|
||||
ui_values: Dictionary of form values from Streamlit
|
||||
"""
|
||||
# Update LLM configuration
|
||||
if "llm_provider" in ui_values:
|
||||
provider = ui_values["llm_provider"]
|
||||
self.config["llm"]["default"] = provider
|
||||
|
||||
# Ensure provider config exists
|
||||
if provider not in self.config["llm"]:
|
||||
self.config["llm"][provider] = {}
|
||||
|
||||
# Update provider-specific config
|
||||
if f"llm_{provider}_api_key" in ui_values and ui_values[f"llm_{provider}_api_key"]:
|
||||
self.config["llm"][provider]["api_key"] = ui_values[f"llm_{provider}_api_key"]
|
||||
|
||||
if f"llm_{provider}_base_url" in ui_values and ui_values[f"llm_{provider}_base_url"]:
|
||||
self.config["llm"][provider]["base_url"] = ui_values[f"llm_{provider}_base_url"]
|
||||
|
||||
if f"llm_{provider}_model" in ui_values and ui_values[f"llm_{provider}_model"]:
|
||||
self.config["llm"][provider]["model"] = ui_values[f"llm_{provider}_model"]
|
||||
|
||||
# Update TTS configuration
|
||||
if "tts_provider" in ui_values:
|
||||
self.config["tts"]["default"] = ui_values["tts_provider"]
|
||||
|
||||
# Update Image configuration
|
||||
if "image_provider" in ui_values:
|
||||
self.config["image"]["default"] = ui_values["image_provider"]
|
||||
|
||||
# Update ComfyKit configuration
|
||||
if "comfykit_mode" in ui_values:
|
||||
if "comfykit" not in self.config["image"]:
|
||||
self.config["image"]["comfykit"] = {}
|
||||
|
||||
mode = ui_values["comfykit_mode"]
|
||||
if mode == "local":
|
||||
if "comfyui_url" in ui_values:
|
||||
self.config["image"]["comfykit"]["comfyui_url"] = ui_values["comfyui_url"]
|
||||
# Remove cloud config
|
||||
self.config["image"]["comfykit"].pop("runninghub_api_key", None)
|
||||
else: # cloud
|
||||
if "runninghub_api_key" in ui_values:
|
||||
self.config["image"]["comfykit"]["runninghub_api_key"] = ui_values["runninghub_api_key"]
|
||||
# Remove local config
|
||||
self.config["image"]["comfykit"].pop("comfyui_url", None)
|
||||
|
||||
self.save()
|
||||
|
||||
def _is_valid_api_key(self, api_key: str) -> bool:
|
||||
"""
|
||||
Check if an API key is valid (not empty or placeholder)
|
||||
|
||||
Args:
|
||||
api_key: API key to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False if empty or placeholder
|
||||
"""
|
||||
if not api_key:
|
||||
return False
|
||||
|
||||
# Remove whitespace
|
||||
api_key = api_key.strip()
|
||||
|
||||
if not api_key:
|
||||
return False
|
||||
|
||||
# Common placeholders to reject
|
||||
invalid_patterns = [
|
||||
"your_", # your_openai_api_key_here, your_dashscope_api_key_here
|
||||
"replace", # replace_me, replace_with_your_key
|
||||
"placeholder",
|
||||
"example_key",
|
||||
]
|
||||
|
||||
api_key_lower = api_key.lower()
|
||||
for pattern in invalid_patterns:
|
||||
if pattern in api_key_lower:
|
||||
return False
|
||||
|
||||
# Accept any non-empty string (allow short keys for testing/custom services)
|
||||
return True
|
||||
|
||||
def validate(self) -> ConfigStatus:
|
||||
"""
|
||||
Validate configuration completeness (simple 3-field format)
|
||||
|
||||
Returns:
|
||||
ConfigStatus with validation results
|
||||
"""
|
||||
status = ConfigStatus()
|
||||
|
||||
# Check LLM configuration (simple format)
|
||||
llm_config = self.config.get("llm", {})
|
||||
|
||||
# Check API key
|
||||
api_key = llm_config.get("api_key", "")
|
||||
if not self._is_valid_api_key(api_key):
|
||||
status.add_missing("llm.api_key")
|
||||
|
||||
# Check base_url
|
||||
base_url = llm_config.get("base_url", "")
|
||||
if not base_url or not base_url.strip():
|
||||
status.add_missing("llm.base_url")
|
||||
|
||||
# Check model
|
||||
model = llm_config.get("model", "")
|
||||
if not model or not model.strip():
|
||||
status.add_missing("llm.model")
|
||||
|
||||
return status
|
||||
|
||||
def get_comfykit_mode(self) -> str:
|
||||
"""
|
||||
Get current ComfyKit mode
|
||||
|
||||
Returns:
|
||||
"local" or "cloud"
|
||||
"""
|
||||
comfykit_config = self.config.get("image", {}).get("comfykit", {})
|
||||
|
||||
if comfykit_config.get("runninghub_api_key"):
|
||||
return "cloud"
|
||||
else:
|
||||
return "local"
|
||||
|
||||
def get_llm_config(self) -> Dict[str, str]:
|
||||
"""
|
||||
Get LLM configuration (simple 3-field format)
|
||||
|
||||
Returns:
|
||||
Dict with api_key, base_url, model
|
||||
"""
|
||||
if self.config is None:
|
||||
return {"api_key": "", "base_url": "", "model": ""}
|
||||
|
||||
llm_config = self.config.get("llm", {})
|
||||
return {
|
||||
"api_key": llm_config.get("api_key", ""),
|
||||
"base_url": llm_config.get("base_url", ""),
|
||||
"model": llm_config.get("model", ""),
|
||||
}
|
||||
|
||||
def set_llm_config(self, api_key: str, base_url: str, model: str):
|
||||
"""
|
||||
Set LLM configuration
|
||||
|
||||
Args:
|
||||
api_key: API key
|
||||
base_url: Base URL
|
||||
model: Model name
|
||||
"""
|
||||
self.config["llm"] = {
|
||||
"api_key": api_key,
|
||||
"base_url": base_url,
|
||||
"model": model,
|
||||
}
|
||||
|
||||
def is_llm_configured(self) -> bool:
|
||||
"""
|
||||
Check if LLM is configured
|
||||
|
||||
Returns:
|
||||
True if all three fields are non-empty
|
||||
"""
|
||||
llm_config = self.get_llm_config()
|
||||
return bool(
|
||||
llm_config["api_key"] and llm_config["api_key"].strip() and
|
||||
llm_config["base_url"] and llm_config["base_url"].strip() and
|
||||
llm_config["model"] and llm_config["model"].strip()
|
||||
)
|
||||
|
||||
def get_tts_providers(self) -> List[str]:
|
||||
"""Get list of available TTS providers"""
|
||||
return ["edge"]
|
||||
|
||||
def get_image_providers(self) -> List[str]:
|
||||
"""Get list of available image providers"""
|
||||
return ["comfykit"]
|
||||
|
||||
def _create_default_config(self) -> Dict[str, Any]:
|
||||
"""Create default configuration"""
|
||||
return {
|
||||
"project_name": "ReelForge",
|
||||
"llm": {
|
||||
"api_key": "", # User must fill in
|
||||
"base_url": "", # User must fill in
|
||||
"model": "", # User must fill in
|
||||
},
|
||||
"tts": {
|
||||
"default": "edge", # Edge TTS is free
|
||||
"edge": {}
|
||||
},
|
||||
"image": {
|
||||
"default": "comfykit",
|
||||
"comfykit": {
|
||||
"comfyui_url": "http://127.0.0.1:8188"
|
||||
}
|
||||
},
|
||||
"mcp_servers": []
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from reelforge.config import load_config
|
||||
from reelforge.config import config_manager
|
||||
from reelforge.services.llm_service import LLMService
|
||||
from reelforge.services.tts_service import TTSService
|
||||
from reelforge.services.image import ImageService
|
||||
@@ -55,7 +55,8 @@ class ReelForgeCore:
|
||||
Args:
|
||||
config_path: Path to configuration file
|
||||
"""
|
||||
self.config = load_config(config_path)
|
||||
# Use global config manager singleton
|
||||
self.config = config_manager.config.to_dict()
|
||||
self._initialized = False
|
||||
|
||||
# Core services (initialized in initialize())
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
"""
|
||||
Lightweight configuration utility for Web UI
|
||||
|
||||
Simple wrapper around config.yaml without heavy dependencies.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import yaml
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class WebConfig:
|
||||
"""Lightweight configuration manager for Web UI"""
|
||||
|
||||
def __init__(self, config_path: str = "config.yaml"):
|
||||
self.config_path = Path(config_path)
|
||||
self.config: Dict[str, Any] = {}
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
"""Load configuration from file"""
|
||||
if not self.config_path.exists():
|
||||
logger.warning(f"Config file not found: {self.config_path}, using default")
|
||||
self.config = self._create_default_config()
|
||||
self.save()
|
||||
else:
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self.config = yaml.safe_load(f) or {}
|
||||
logger.info(f"Configuration loaded from {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
self.config = self._create_default_config()
|
||||
|
||||
def save(self):
|
||||
"""Save configuration to file"""
|
||||
try:
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(self.config, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
logger.info(f"Configuration saved to {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""
|
||||
Validate configuration completeness
|
||||
|
||||
Returns:
|
||||
True if required fields are present
|
||||
"""
|
||||
# Check LLM configuration (required)
|
||||
llm_config = self.config.get("llm", {})
|
||||
if not all([
|
||||
llm_config.get("api_key"),
|
||||
llm_config.get("base_url"),
|
||||
llm_config.get("model")
|
||||
]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_llm_config(self) -> Dict[str, str]:
|
||||
"""Get LLM configuration"""
|
||||
llm = self.config.get("llm", {})
|
||||
return {
|
||||
"api_key": llm.get("api_key", ""),
|
||||
"base_url": llm.get("base_url", ""),
|
||||
"model": llm.get("model", "")
|
||||
}
|
||||
|
||||
def set_llm_config(self, api_key: str, base_url: str, model: str):
|
||||
"""Set LLM configuration"""
|
||||
if "llm" not in self.config:
|
||||
self.config["llm"] = {}
|
||||
|
||||
self.config["llm"]["api_key"] = api_key
|
||||
self.config["llm"]["base_url"] = base_url
|
||||
self.config["llm"]["model"] = model
|
||||
|
||||
def get_image_config(self) -> Dict[str, Any]:
|
||||
"""Get image generation configuration"""
|
||||
return self.config.get("image", {})
|
||||
|
||||
def set_image_config(self, comfyui_url: Optional[str] = None, runninghub_api_key: Optional[str] = None):
|
||||
"""Set image generation configuration"""
|
||||
if "image" not in self.config:
|
||||
self.config["image"] = {}
|
||||
|
||||
if comfyui_url is not None:
|
||||
self.config["image"]["comfyui_url"] = comfyui_url
|
||||
|
||||
if runninghub_api_key is not None:
|
||||
self.config["image"]["runninghub_api_key"] = runninghub_api_key
|
||||
|
||||
def _create_default_config(self) -> Dict[str, Any]:
|
||||
"""Create default configuration"""
|
||||
return {
|
||||
"project_name": "ReelForge",
|
||||
"llm": {
|
||||
"api_key": "",
|
||||
"base_url": "",
|
||||
"model": ""
|
||||
},
|
||||
"tts": {
|
||||
"default_workflow": "edge"
|
||||
},
|
||||
"image": {
|
||||
"comfyui_url": "http://127.0.0.1:8188",
|
||||
"runninghub_api_key": "",
|
||||
"prompt_prefix": "Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines"
|
||||
}
|
||||
}
|
||||
|
||||
26
web.py
26
web.py
@@ -14,7 +14,7 @@ from loguru import logger
|
||||
|
||||
# Import i18n and config manager
|
||||
from reelforge.i18n import load_locales, set_language, tr, get_available_languages
|
||||
from reelforge.utils.web_config import WebConfig
|
||||
from reelforge.config import config_manager
|
||||
from reelforge.models.progress import ProgressEvent
|
||||
|
||||
# Setup page config (must be first)
|
||||
@@ -47,9 +47,7 @@ def safe_rerun():
|
||||
# Configuration & i18n Initialization
|
||||
# ============================================================================
|
||||
|
||||
def get_config_manager():
|
||||
"""Get WebConfig instance (no caching - always fresh)"""
|
||||
return WebConfig()
|
||||
# Config manager is already a global singleton, use it directly
|
||||
|
||||
|
||||
def init_i18n():
|
||||
@@ -127,7 +125,7 @@ def init_session_state():
|
||||
# System Configuration (Required)
|
||||
# ============================================================================
|
||||
|
||||
def render_advanced_settings(config_manager: WebConfig):
|
||||
def render_advanced_settings():
|
||||
"""Render system configuration (required) with 2-column layout"""
|
||||
# Check if system is configured
|
||||
is_configured = config_manager.validate()
|
||||
@@ -300,17 +298,9 @@ def render_advanced_settings(config_manager: WebConfig):
|
||||
|
||||
with col2:
|
||||
if st.button(tr("btn.reset_config"), use_container_width=True, key="reset_config_btn"):
|
||||
# Reset to default by creating new config
|
||||
config_manager.config = {
|
||||
"project_name": "ReelForge",
|
||||
"llm": {"api_key": "", "base_url": "", "model": ""},
|
||||
"tts": {"default_workflow": "edge"},
|
||||
"image": {
|
||||
"comfyui_url": "http://127.0.0.1:8188",
|
||||
"runninghub_api_key": "",
|
||||
"prompt_prefix": "Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines"
|
||||
}
|
||||
}
|
||||
# Reset to default
|
||||
from reelforge.config.schema import ReelForgeConfig
|
||||
config_manager.config = ReelForgeConfig()
|
||||
config_manager.save()
|
||||
st.success(tr("status.config_reset"))
|
||||
safe_rerun()
|
||||
@@ -351,8 +341,6 @@ def main():
|
||||
init_session_state()
|
||||
init_i18n()
|
||||
|
||||
config_manager = get_config_manager()
|
||||
|
||||
# Top bar: Title + Language selector
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
@@ -367,7 +355,7 @@ def main():
|
||||
# System Configuration (Required)
|
||||
# Auto-expands if not configured, collapses if configured
|
||||
# ========================================================================
|
||||
render_advanced_settings(config_manager)
|
||||
render_advanced_settings()
|
||||
|
||||
# Three-column layout
|
||||
left_col, middle_col, right_col = st.columns([1, 1, 1])
|
||||
|
||||
Reference in New Issue
Block a user