重构配置逻辑
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,6 +41,8 @@ Thumbs.db
|
|||||||
|
|
||||||
# Config files with sensitive data
|
# Config files with sensitive data
|
||||||
config.yaml
|
config.yaml
|
||||||
|
config.yaml.bak
|
||||||
|
*.yaml.bak
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
@@ -1,108 +1,29 @@
|
|||||||
# ReelForge Configuration Example
|
# ReelForge Configuration
|
||||||
# 复制此文件为 config.yaml 并填入你的配置
|
# Copy this file to config.yaml and fill in your settings
|
||||||
|
# ⚠️ Never commit config.yaml to Git!
|
||||||
|
|
||||||
# Project name
|
|
||||||
project_name: ReelForge
|
project_name: ReelForge
|
||||||
|
|
||||||
# ==================== LLM Configuration ====================
|
# ==================== LLM Configuration ====================
|
||||||
# Simple 3-field configuration - works with any OpenAI SDK compatible LLM
|
# Supports any OpenAI SDK compatible API
|
||||||
#
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
llm:
|
llm:
|
||||||
api_key: "" # Fill in your API key
|
api_key: ""
|
||||||
base_url: "" # LLM API endpoint
|
base_url: ""
|
||||||
model: "" # Model name
|
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 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:
|
tts:
|
||||||
default_workflow: "edge" # Default: "edge" (Edge TTS) or "tts_default.json" (ComfyUI workflow)
|
default: edge # "edge" (free) or "tts_xxx.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
|
|
||||||
|
|
||||||
# ==================== Image Generation Configuration ====================
|
# ==================== Image Generation Configuration ====================
|
||||||
# Image generation uses ComfyUI workflows
|
|
||||||
# Workflows are auto-discovered from workflows/image_*.json files
|
|
||||||
image:
|
image:
|
||||||
default: default # Default preset name (uses workflows/image_default.json)
|
default: image_default.json
|
||||||
comfyui_url: http://127.0.0.1:8188 # Local ComfyUI server
|
comfyui_url: http://127.0.0.1:8188
|
||||||
# runninghub_api_key: "" # Optional: RunningHub cloud API key
|
runninghub_api_key: ""
|
||||||
|
|
||||||
# Prompt prefix - automatically added to all image prompts
|
|
||||||
# Leave empty ("") if you don't want any prefix
|
|
||||||
prompt_prefix: "Pure white background, minimalist illustration, matchstick figure style, black and white line drawing, simple clean lines"
|
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:
|
Usage:
|
||||||
from reelforge import reelforge
|
from reelforge import reelforge
|
||||||
@@ -18,8 +18,9 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from reelforge.service import ReelForgeCore, reelforge
|
from reelforge.service import ReelForgeCore, reelforge
|
||||||
|
from reelforge.config import config_manager
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__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 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.llm_service import LLMService
|
||||||
from reelforge.services.tts_service import TTSService
|
from reelforge.services.tts_service import TTSService
|
||||||
from reelforge.services.image import ImageService
|
from reelforge.services.image import ImageService
|
||||||
@@ -55,7 +55,8 @@ class ReelForgeCore:
|
|||||||
Args:
|
Args:
|
||||||
config_path: Path to configuration file
|
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
|
self._initialized = False
|
||||||
|
|
||||||
# Core services (initialized in initialize())
|
# 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
|
# Import i18n and config manager
|
||||||
from reelforge.i18n import load_locales, set_language, tr, get_available_languages
|
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
|
from reelforge.models.progress import ProgressEvent
|
||||||
|
|
||||||
# Setup page config (must be first)
|
# Setup page config (must be first)
|
||||||
@@ -47,9 +47,7 @@ def safe_rerun():
|
|||||||
# Configuration & i18n Initialization
|
# Configuration & i18n Initialization
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def get_config_manager():
|
# Config manager is already a global singleton, use it directly
|
||||||
"""Get WebConfig instance (no caching - always fresh)"""
|
|
||||||
return WebConfig()
|
|
||||||
|
|
||||||
|
|
||||||
def init_i18n():
|
def init_i18n():
|
||||||
@@ -127,7 +125,7 @@ def init_session_state():
|
|||||||
# System Configuration (Required)
|
# System Configuration (Required)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def render_advanced_settings(config_manager: WebConfig):
|
def render_advanced_settings():
|
||||||
"""Render system configuration (required) with 2-column layout"""
|
"""Render system configuration (required) with 2-column layout"""
|
||||||
# Check if system is configured
|
# Check if system is configured
|
||||||
is_configured = config_manager.validate()
|
is_configured = config_manager.validate()
|
||||||
@@ -300,17 +298,9 @@ def render_advanced_settings(config_manager: WebConfig):
|
|||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if st.button(tr("btn.reset_config"), use_container_width=True, key="reset_config_btn"):
|
if st.button(tr("btn.reset_config"), use_container_width=True, key="reset_config_btn"):
|
||||||
# Reset to default by creating new config
|
# Reset to default
|
||||||
config_manager.config = {
|
from reelforge.config.schema import ReelForgeConfig
|
||||||
"project_name": "ReelForge",
|
config_manager.config = ReelForgeConfig()
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config_manager.save()
|
config_manager.save()
|
||||||
st.success(tr("status.config_reset"))
|
st.success(tr("status.config_reset"))
|
||||||
safe_rerun()
|
safe_rerun()
|
||||||
@@ -351,8 +341,6 @@ def main():
|
|||||||
init_session_state()
|
init_session_state()
|
||||||
init_i18n()
|
init_i18n()
|
||||||
|
|
||||||
config_manager = get_config_manager()
|
|
||||||
|
|
||||||
# Top bar: Title + Language selector
|
# Top bar: Title + Language selector
|
||||||
col1, col2 = st.columns([4, 1])
|
col1, col2 = st.columns([4, 1])
|
||||||
with col1:
|
with col1:
|
||||||
@@ -367,7 +355,7 @@ def main():
|
|||||||
# System Configuration (Required)
|
# System Configuration (Required)
|
||||||
# Auto-expands if not configured, collapses if configured
|
# Auto-expands if not configured, collapses if configured
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
render_advanced_settings(config_manager)
|
render_advanced_settings()
|
||||||
|
|
||||||
# Three-column layout
|
# Three-column layout
|
||||||
left_col, middle_col, right_col = st.columns([1, 1, 1])
|
left_col, middle_col, right_col = st.columns([1, 1, 1])
|
||||||
|
|||||||
Reference in New Issue
Block a user