499 lines
25 KiB
Python
499 lines
25 KiB
Python
# Copyright (C) 2025 AIDC-AI
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
System settings component for web UI
|
|
"""
|
|
|
|
import streamlit as st
|
|
|
|
from web.i18n import tr, get_language
|
|
from web.utils.streamlit_helpers import safe_rerun
|
|
from pixelle_video.config import config_manager
|
|
|
|
|
|
def render_advanced_settings():
|
|
"""Render system configuration (required) with 2-column layout"""
|
|
# Check if system is configured
|
|
is_configured = config_manager.validate()
|
|
|
|
# Expand if not configured, collapse if configured
|
|
with st.expander(tr("settings.title"), expanded=not is_configured):
|
|
# 2-column layout: LLM | ComfyUI, followed by direct media API providers.
|
|
llm_col, comfyui_col = st.columns(2)
|
|
|
|
# ====================================================================
|
|
# Column 1: LLM Settings
|
|
# ====================================================================
|
|
with llm_col:
|
|
with st.container(border=True):
|
|
st.markdown(f"**{tr('settings.llm.title')}**")
|
|
|
|
# Quick preset selection
|
|
from pixelle_video.llm_presets import get_preset_names, get_preset, find_preset_by_base_url_and_model
|
|
|
|
# Custom at the end
|
|
preset_names = get_preset_names() + ["Custom"]
|
|
|
|
# Get current config
|
|
current_llm = config_manager.get_llm_config()
|
|
|
|
# Auto-detect which preset matches current config
|
|
current_preset = find_preset_by_base_url_and_model(
|
|
current_llm["base_url"],
|
|
current_llm["model"]
|
|
)
|
|
|
|
# Determine default index based on current config
|
|
if current_preset:
|
|
# Current config matches a preset
|
|
default_index = preset_names.index(current_preset)
|
|
else:
|
|
# Current config doesn't match any preset -> Custom
|
|
default_index = len(preset_names) - 1
|
|
|
|
selected_preset = st.selectbox(
|
|
tr("settings.llm.quick_select"),
|
|
options=preset_names,
|
|
index=default_index,
|
|
help=tr("settings.llm.quick_select_help"),
|
|
key="llm_preset_select"
|
|
)
|
|
|
|
# Auto-fill based on selected preset
|
|
if selected_preset != "Custom":
|
|
# Preset selected
|
|
preset_config = get_preset(selected_preset)
|
|
|
|
# If user switched to a different preset (not current one), clear API key
|
|
# If it's the same as current config, keep API key
|
|
if selected_preset == current_preset:
|
|
# Same preset as saved config: keep API key
|
|
default_api_key = current_llm["api_key"]
|
|
else:
|
|
# Different preset: use default_api_key if provided (e.g., Ollama), otherwise clear
|
|
default_api_key = preset_config.get("default_api_key", "")
|
|
|
|
default_base_url = preset_config.get("base_url", "")
|
|
default_model = preset_config.get("model", "")
|
|
|
|
# Show API key URL if available
|
|
if preset_config.get("api_key_url"):
|
|
st.markdown(f"🔑 [{tr('settings.llm.get_api_key')}]({preset_config['api_key_url']})")
|
|
else:
|
|
# Custom: show current saved config (if any)
|
|
default_api_key = current_llm["api_key"]
|
|
default_base_url = current_llm["base_url"]
|
|
default_model = current_llm["model"]
|
|
|
|
st.markdown("---")
|
|
|
|
# API Key (use unique key to force refresh when switching preset)
|
|
llm_api_key = st.text_input(
|
|
f"{tr('settings.llm.api_key')} *",
|
|
value=default_api_key,
|
|
type="password",
|
|
help=tr("settings.llm.api_key_help"),
|
|
key=f"llm_api_key_input_{selected_preset}"
|
|
)
|
|
|
|
# Base URL (use unique key based on preset to force refresh)
|
|
llm_base_url = st.text_input(
|
|
f"{tr('settings.llm.base_url')} *",
|
|
value=default_base_url,
|
|
help=tr("settings.llm.base_url_help"),
|
|
key=f"llm_base_url_input_{selected_preset}"
|
|
)
|
|
|
|
# Model selection with dropdown and load button
|
|
# Initialize session state for loaded models
|
|
if "llm_loaded_models" not in st.session_state:
|
|
st.session_state.llm_loaded_models = []
|
|
|
|
# Build model options: Custom option + loaded models
|
|
CUSTOM_MODEL_OPTION = f"✏️ {tr('settings.llm.custom_model')}"
|
|
model_options = [CUSTOM_MODEL_OPTION] + st.session_state.llm_loaded_models
|
|
|
|
# Determine default selection
|
|
if default_model in st.session_state.llm_loaded_models:
|
|
default_model_index = model_options.index(default_model)
|
|
else:
|
|
# Default model not in loaded list, use custom
|
|
default_model_index = 0
|
|
|
|
# Model dropdown with load button on the right
|
|
model_col, load_col, test_col = st.columns([3, 1, 1])
|
|
|
|
with model_col:
|
|
selected_model_option = st.selectbox(
|
|
f"{tr('settings.llm.model')} *",
|
|
options=model_options,
|
|
index=default_model_index,
|
|
help=tr("settings.llm.model_help"),
|
|
key=f"llm_model_select_{selected_preset}"
|
|
)
|
|
|
|
with load_col:
|
|
st.markdown("<div style='height: 28px'></div>", unsafe_allow_html=True)
|
|
load_clicked = st.button(
|
|
f"🔄 {tr('settings.llm.load_models')}",
|
|
help=tr("settings.llm.load_models_help"),
|
|
key="load_models_btn",
|
|
use_container_width=True
|
|
)
|
|
|
|
with test_col:
|
|
st.markdown("<div style='height: 28px'></div>", unsafe_allow_html=True)
|
|
test_clicked = st.button(
|
|
f"🔌 {tr('settings.llm.test_connection')}",
|
|
help=tr("settings.llm.test_connection_help"),
|
|
key="test_llm_connection_btn",
|
|
use_container_width=True
|
|
)
|
|
|
|
# Handle load models button click
|
|
if load_clicked:
|
|
if llm_api_key and llm_base_url:
|
|
try:
|
|
from pixelle_video.utils.llm_util import fetch_available_models
|
|
with st.spinner(tr("settings.llm.loading_models")):
|
|
models = fetch_available_models(llm_api_key, llm_base_url)
|
|
st.session_state.llm_loaded_models = models
|
|
st.success(tr("settings.llm.models_loaded").replace("{count}", str(len(models))))
|
|
safe_rerun()
|
|
except Exception as e:
|
|
st.error(tr("settings.llm.models_load_failed").replace("{error}", str(e)))
|
|
else:
|
|
st.warning(tr("status.llm_config_incomplete"))
|
|
|
|
# Handle test connection button click
|
|
if test_clicked:
|
|
if llm_api_key and llm_base_url:
|
|
try:
|
|
from pixelle_video.utils.llm_util import test_llm_connection
|
|
with st.spinner(tr("settings.llm.loading_models")):
|
|
success, message, model_count = test_llm_connection(llm_api_key, llm_base_url)
|
|
if success:
|
|
st.success(tr("settings.llm.connection_success").replace("{count}", str(model_count)))
|
|
else:
|
|
st.error(tr("settings.llm.connection_failed").replace("{error}", message))
|
|
except Exception as e:
|
|
st.error(tr("settings.llm.connection_failed").replace("{error}", str(e)))
|
|
else:
|
|
st.warning(tr("status.llm_config_incomplete"))
|
|
|
|
# If custom option selected, show text input for custom model name
|
|
if selected_model_option == CUSTOM_MODEL_OPTION:
|
|
llm_model = st.text_input(
|
|
tr("settings.llm.custom_model_input"),
|
|
value=default_model,
|
|
help=tr("settings.llm.model_help"),
|
|
key=f"llm_custom_model_input_{selected_preset}"
|
|
)
|
|
else:
|
|
llm_model = selected_model_option
|
|
|
|
# ====================================================================
|
|
# Column 2: ComfyUI Settings
|
|
# ====================================================================
|
|
with comfyui_col:
|
|
with st.container(border=True):
|
|
st.markdown(f"**{tr('settings.comfyui.title')}**")
|
|
|
|
# Get current configuration
|
|
comfyui_config = config_manager.get_comfyui_config()
|
|
|
|
# Local/Self-hosted ComfyUI configuration
|
|
st.markdown(f"**{tr('settings.comfyui.local_title')}**")
|
|
url_col, key_col = st.columns(2)
|
|
with url_col:
|
|
comfyui_url = st.text_input(
|
|
tr("settings.comfyui.comfyui_url"),
|
|
value=comfyui_config.get("comfyui_url", "http://127.0.0.1:8188"),
|
|
help=tr("settings.comfyui.comfyui_url_help"),
|
|
key="comfyui_url_input"
|
|
)
|
|
with key_col:
|
|
comfyui_api_key = st.text_input(
|
|
tr("settings.comfyui.comfyui_api_key"),
|
|
value=comfyui_config.get("comfyui_api_key", ""),
|
|
type="password",
|
|
help=tr("settings.comfyui.comfyui_api_key_help"),
|
|
key="comfyui_api_key_input"
|
|
)
|
|
|
|
# Test connection button
|
|
if st.button(tr("btn.test_connection"), key="test_comfyui", use_container_width=True):
|
|
try:
|
|
import requests
|
|
response = requests.get(f"{comfyui_url}/system_stats", timeout=5)
|
|
if response.status_code == 200:
|
|
st.success(tr("status.connection_success"))
|
|
else:
|
|
st.error(tr("status.connection_failed"))
|
|
except Exception as e:
|
|
st.error(f"{tr('status.connection_failed')}: {str(e)}")
|
|
|
|
st.markdown("---")
|
|
|
|
# RunningHub cloud configuration
|
|
st.markdown(f"**{tr('settings.comfyui.cloud_title')}**")
|
|
runninghub_api_key = st.text_input(
|
|
tr("settings.comfyui.runninghub_api_key"),
|
|
value=comfyui_config.get("runninghub_api_key", ""),
|
|
type="password",
|
|
help=tr("settings.comfyui.runninghub_api_key_help"),
|
|
key="runninghub_api_key_input"
|
|
)
|
|
st.caption(
|
|
f"{tr('settings.comfyui.runninghub_hint')} "
|
|
f"[{tr('settings.comfyui.runninghub_get_api_key')}]"
|
|
f"(https://www.runninghub{'.cn' if get_language() == 'zh_CN' else '.ai'}/?inviteCode=bozpdlbj)"
|
|
)
|
|
|
|
# RunningHub concurrent limit and instance type (in one row)
|
|
limit_col, instance_col = st.columns(2)
|
|
with limit_col:
|
|
runninghub_concurrent_limit = st.number_input(
|
|
tr("settings.comfyui.runninghub_concurrent_limit"),
|
|
min_value=1,
|
|
max_value=10,
|
|
value=comfyui_config.get("runninghub_concurrent_limit", 1),
|
|
help=tr("settings.comfyui.runninghub_concurrent_limit_help"),
|
|
key="runninghub_concurrent_limit_input"
|
|
)
|
|
with instance_col:
|
|
# Check if instance type is "plus" (48G VRAM enabled)
|
|
current_instance_type = comfyui_config.get("runninghub_instance_type") or ""
|
|
is_plus_enabled = current_instance_type == "plus"
|
|
# Instance type options with i18n
|
|
instance_options = [
|
|
tr("settings.comfyui.runninghub_instance_24g"),
|
|
tr("settings.comfyui.runninghub_instance_48g"),
|
|
]
|
|
runninghub_instance_type_display = st.selectbox(
|
|
tr("settings.comfyui.runninghub_instance_type"),
|
|
options=instance_options,
|
|
index=1 if is_plus_enabled else 0,
|
|
help=tr("settings.comfyui.runninghub_instance_type_help"),
|
|
key="runninghub_instance_type_input"
|
|
)
|
|
# Convert display value back to actual value
|
|
runninghub_48g_enabled = runninghub_instance_type_display == tr("settings.comfyui.runninghub_instance_48g")
|
|
|
|
# ====================================================================
|
|
# Direct API media providers
|
|
# ====================================================================
|
|
zh = get_language() == "zh_CN"
|
|
api_cfg = config_manager.get_api_providers_config()
|
|
common_cfg = api_cfg.get("common", {})
|
|
openai_cfg = api_cfg.get("openai", {})
|
|
dashscope_cfg = api_cfg.get("dashscope", {})
|
|
ark_cfg = api_cfg.get("ark", {})
|
|
kling_cfg = api_cfg.get("kling", {})
|
|
default_api_base_urls = {
|
|
"openai": "https://api.openai.com/v1",
|
|
"dashscope": "https://dashscope.aliyuncs.com/api/v1",
|
|
"ark": "https://ark.cn-beijing.volces.com/api/v3",
|
|
"kling": "https://api-beijing.klingai.com",
|
|
}
|
|
|
|
with st.container(border=True):
|
|
st.markdown("**🧩 API 媒体模型**" if zh else "**🧩 API Media Models**")
|
|
st.caption(
|
|
"用于直连图像/视频模型,不影响上方 LLM 与 ComfyUI/RunningHub 配置。"
|
|
if zh
|
|
else "Used for direct image/video model calls. This does not affect the LLM or ComfyUI/RunningHub settings above."
|
|
)
|
|
|
|
common_col, proxy_col = st.columns(2)
|
|
with common_col:
|
|
api_print_model_input = st.checkbox(
|
|
"打印模型请求参数" if zh else "Print model request parameters",
|
|
value=bool(common_cfg.get("print_model_input", False)),
|
|
help=(
|
|
"调试用。开启后会在终端打印发送给图像/视频模型的 prompt、模型名和输入文件路径。"
|
|
if zh
|
|
else "For debugging. Prints prompts, model names and input file paths sent to image/video models."
|
|
),
|
|
key="api_media_print_model_input",
|
|
)
|
|
with proxy_col:
|
|
api_local_proxy = st.text_input(
|
|
"本地代理(可选)" if zh else "Local proxy (optional)",
|
|
value=common_cfg.get("local_proxy", ""),
|
|
placeholder="http://127.0.0.1:9090",
|
|
help=(
|
|
"仅部分提供商会使用,例如 OpenAI 图像模型。留空表示不使用代理。"
|
|
if zh
|
|
else "Only used by some providers, such as OpenAI image models. Leave blank to disable."
|
|
),
|
|
key="api_media_local_proxy",
|
|
)
|
|
|
|
st.markdown("---")
|
|
|
|
provider_col1, provider_col2 = st.columns(2)
|
|
with provider_col1:
|
|
st.markdown("**OpenAI / GPT Image**")
|
|
api_openai_use_proxy = st.checkbox(
|
|
"OpenAI 启用代理" if zh else "Use proxy for OpenAI",
|
|
value=bool(openai_cfg.get("use_proxy", False)),
|
|
key="api_media_openai_use_proxy",
|
|
)
|
|
api_openai_key = st.text_input(
|
|
"OpenAI API Key",
|
|
value=openai_cfg.get("api_key", ""),
|
|
type="password",
|
|
key="api_media_openai_key",
|
|
)
|
|
api_openai_base_url = st.text_input(
|
|
"OpenAI Base URL",
|
|
value=openai_cfg.get("base_url") or default_api_base_urls["openai"],
|
|
placeholder="https://api.openai.com/v1",
|
|
key="api_media_openai_base_url",
|
|
)
|
|
|
|
st.markdown("**DashScope / Wan / HappyHorse**")
|
|
api_dashscope_use_proxy = st.checkbox(
|
|
"DashScope 启用代理" if zh else "Use proxy for DashScope",
|
|
value=bool(dashscope_cfg.get("use_proxy", False)),
|
|
key="api_media_dashscope_use_proxy",
|
|
)
|
|
api_dashscope_key = st.text_input(
|
|
"DashScope API Key",
|
|
value=dashscope_cfg.get("api_key", ""),
|
|
type="password",
|
|
key="api_media_dashscope_key",
|
|
)
|
|
api_dashscope_base_url = st.text_input(
|
|
"DashScope Base URL",
|
|
value=dashscope_cfg.get("base_url") or default_api_base_urls["dashscope"],
|
|
placeholder="https://dashscope.aliyuncs.com/api/v1",
|
|
key="api_media_dashscope_base_url",
|
|
)
|
|
|
|
with provider_col2:
|
|
st.markdown("**Volcengine ARK / Seedream / Seedance**")
|
|
api_ark_use_proxy = st.checkbox(
|
|
"ARK 启用代理" if zh else "Use proxy for ARK",
|
|
value=bool(ark_cfg.get("use_proxy", False)),
|
|
key="api_media_ark_use_proxy",
|
|
)
|
|
api_ark_key = st.text_input(
|
|
"ARK API Key",
|
|
value=ark_cfg.get("api_key", ""),
|
|
type="password",
|
|
key="api_media_ark_key",
|
|
)
|
|
api_ark_base_url = st.text_input(
|
|
"ARK Base URL",
|
|
value=ark_cfg.get("base_url") or default_api_base_urls["ark"],
|
|
placeholder="https://ark.cn-beijing.volces.com/api/v3",
|
|
key="api_media_ark_base_url",
|
|
)
|
|
|
|
st.markdown("**Kling AI / 可灵**")
|
|
api_kling_use_proxy = st.checkbox(
|
|
"Kling 启用代理" if zh else "Use proxy for Kling",
|
|
value=bool(kling_cfg.get("use_proxy", False)),
|
|
key="api_media_kling_use_proxy",
|
|
)
|
|
api_kling_base_url = st.text_input(
|
|
"Kling Base URL",
|
|
value=kling_cfg.get("base_url") or default_api_base_urls["kling"],
|
|
placeholder="https://api-beijing.klingai.com",
|
|
key="api_media_kling_base_url",
|
|
)
|
|
api_kling_access_key = st.text_input(
|
|
"Kling Access Key",
|
|
value=kling_cfg.get("access_key", ""),
|
|
type="password",
|
|
key="api_media_kling_access_key",
|
|
)
|
|
api_kling_secret_key = st.text_input(
|
|
"Kling Secret Key",
|
|
value=kling_cfg.get("secret_key", ""),
|
|
type="password",
|
|
key="api_media_kling_secret_key",
|
|
)
|
|
|
|
# ====================================================================
|
|
# Action Buttons (full width at bottom)
|
|
# ====================================================================
|
|
st.markdown("---")
|
|
|
|
col1, col2 = st.columns(2)
|
|
with col1:
|
|
if st.button(tr("btn.save_config"), use_container_width=True, key="save_config_btn"):
|
|
try:
|
|
# Validate and save LLM configuration
|
|
if not (llm_api_key and llm_base_url and llm_model):
|
|
st.error(tr("status.llm_config_incomplete"))
|
|
else:
|
|
config_manager.set_llm_config(llm_api_key, llm_base_url, llm_model)
|
|
|
|
# Save ComfyUI configuration (optional fields, always save what's provided)
|
|
# Convert checkbox to instance type: True -> "plus", False -> ""
|
|
instance_type = "plus" if runninghub_48g_enabled else ""
|
|
config_manager.set_comfyui_config(
|
|
comfyui_url=comfyui_url if comfyui_url else None,
|
|
comfyui_api_key=comfyui_api_key if comfyui_api_key else None,
|
|
runninghub_api_key=runninghub_api_key if runninghub_api_key else None,
|
|
runninghub_concurrent_limit=int(runninghub_concurrent_limit),
|
|
runninghub_instance_type=instance_type
|
|
)
|
|
|
|
# Save direct image/video API provider configuration.
|
|
config_manager.set_api_provider_config("common", {
|
|
"print_model_input": bool(api_print_model_input),
|
|
"local_proxy": api_local_proxy or "",
|
|
})
|
|
config_manager.set_api_provider_config("openai", {
|
|
"api_key": api_openai_key or "",
|
|
"base_url": api_openai_base_url or "",
|
|
"use_proxy": bool(api_openai_use_proxy),
|
|
})
|
|
config_manager.set_api_provider_config("dashscope", {
|
|
"api_key": api_dashscope_key or "",
|
|
"base_url": api_dashscope_base_url or "",
|
|
"use_proxy": bool(api_dashscope_use_proxy),
|
|
})
|
|
config_manager.set_api_provider_config("ark", {
|
|
"api_key": api_ark_key or "",
|
|
"base_url": api_ark_base_url or "",
|
|
"use_proxy": bool(api_ark_use_proxy),
|
|
})
|
|
config_manager.set_api_provider_config("kling", {
|
|
"base_url": api_kling_base_url or "",
|
|
"access_key": api_kling_access_key or "",
|
|
"secret_key": api_kling_secret_key or "",
|
|
"use_proxy": bool(api_kling_use_proxy),
|
|
})
|
|
|
|
# Only save to file if LLM config is valid
|
|
if llm_api_key and llm_base_url and llm_model:
|
|
config_manager.save()
|
|
st.success(tr("status.config_saved"))
|
|
safe_rerun()
|
|
except Exception as e:
|
|
st.error(f"{tr('status.save_failed')}: {str(e)}")
|
|
|
|
with col2:
|
|
if st.button(tr("btn.reset_config"), use_container_width=True, key="reset_config_btn"):
|
|
# Reset to default
|
|
from pixelle_video.config.schema import PixelleVideoConfig
|
|
config_manager.config = PixelleVideoConfig()
|
|
config_manager.save()
|
|
st.success(tr("status.config_reset"))
|
|
safe_rerun()
|