返回 Pixelle-Video
settings.py
根目录 / web / components / settings.py
1 # Copyright (C) 2025 AIDC-AI
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 # http://www.apache.org/licenses/LICENSE-2.0
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12
13 """
14 System settings component for web UI
15 """
16
17 import streamlit as st
18
19 from web.i18n import tr, get_language
20 from web.utils.streamlit_helpers import safe_rerun
21 from pixelle_video.config import config_manager
22
23
24 def render_advanced_settings():
25 """Render system configuration (required) with 2-column layout"""
26 # Check if system is configured
27 is_configured = config_manager.validate()
28
29 # Expand if not configured, collapse if configured
30 with st.expander(tr("settings.title"), expanded=not is_configured):
31 # 2-column layout: LLM | ComfyUI, followed by direct media API providers.
32 llm_col, comfyui_col = st.columns(2)
33
34 # ====================================================================
35 # Column 1: LLM Settings
36 # ====================================================================
37 with llm_col:
38 with st.container(border=True):
39 st.markdown(f"**{tr('settings.llm.title')}**")
40
41 # Quick preset selection
42 from pixelle_video.llm_presets import get_preset_names, get_preset, find_preset_by_base_url_and_model
43
44 # Custom at the end
45 preset_names = get_preset_names() + ["Custom"]
46
47 # Get current config
48 current_llm = config_manager.get_llm_config()
49
50 # Auto-detect which preset matches current config
51 current_preset = find_preset_by_base_url_and_model(
52 current_llm["base_url"],
53 current_llm["model"]
54 )
55
56 # Determine default index based on current config
57 if current_preset:
58 # Current config matches a preset
59 default_index = preset_names.index(current_preset)
60 else:
61 # Current config doesn't match any preset -> Custom
62 default_index = len(preset_names) - 1
63
64 selected_preset = st.selectbox(
65 tr("settings.llm.quick_select"),
66 options=preset_names,
67 index=default_index,
68 help=tr("settings.llm.quick_select_help"),
69 key="llm_preset_select"
70 )
71
72 # Auto-fill based on selected preset
73 if selected_preset != "Custom":
74 # Preset selected
75 preset_config = get_preset(selected_preset)
76
77 # If user switched to a different preset (not current one), clear API key
78 # If it's the same as current config, keep API key
79 if selected_preset == current_preset:
80 # Same preset as saved config: keep API key
81 default_api_key = current_llm["api_key"]
82 else:
83 # Different preset: use default_api_key if provided (e.g., Ollama), otherwise clear
84 default_api_key = preset_config.get("default_api_key", "")
85
86 default_base_url = preset_config.get("base_url", "")
87 default_model = preset_config.get("model", "")
88
89 # Show API key URL if available
90 if preset_config.get("api_key_url"):
91 st.markdown(f"🔑 [{tr('settings.llm.get_api_key')}]({preset_config['api_key_url']})")
92 else:
93 # Custom: show current saved config (if any)
94 default_api_key = current_llm["api_key"]
95 default_base_url = current_llm["base_url"]
96 default_model = current_llm["model"]
97
98 st.markdown("---")
99
100 # API Key (use unique key to force refresh when switching preset)
101 llm_api_key = st.text_input(
102 f"{tr('settings.llm.api_key')} *",
103 value=default_api_key,
104 type="password",
105 help=tr("settings.llm.api_key_help"),
106 key=f"llm_api_key_input_{selected_preset}"
107 )
108
109 # Base URL (use unique key based on preset to force refresh)
110 llm_base_url = st.text_input(
111 f"{tr('settings.llm.base_url')} *",
112 value=default_base_url,
113 help=tr("settings.llm.base_url_help"),
114 key=f"llm_base_url_input_{selected_preset}"
115 )
116
117 # Model selection with dropdown and load button
118 # Initialize session state for loaded models
119 if "llm_loaded_models" not in st.session_state:
120 st.session_state.llm_loaded_models = []
121
122 # Build model options: Custom option + loaded models
123 CUSTOM_MODEL_OPTION = f"✏️ {tr('settings.llm.custom_model')}"
124 model_options = [CUSTOM_MODEL_OPTION] + st.session_state.llm_loaded_models
125
126 # Determine default selection
127 if default_model in st.session_state.llm_loaded_models:
128 default_model_index = model_options.index(default_model)
129 else:
130 # Default model not in loaded list, use custom
131 default_model_index = 0
132
133 # Model dropdown with load button on the right
134 model_col, load_col, test_col = st.columns([3, 1, 1])
135
136 with model_col:
137 selected_model_option = st.selectbox(
138 f"{tr('settings.llm.model')} *",
139 options=model_options,
140 index=default_model_index,
141 help=tr("settings.llm.model_help"),
142 key=f"llm_model_select_{selected_preset}"
143 )
144
145 with load_col:
146 st.markdown("<div style='height: 28px'></div>", unsafe_allow_html=True)
147 load_clicked = st.button(
148 f"🔄 {tr('settings.llm.load_models')}",
149 help=tr("settings.llm.load_models_help"),
150 key="load_models_btn",
151 use_container_width=True
152 )
153
154 with test_col:
155 st.markdown("<div style='height: 28px'></div>", unsafe_allow_html=True)
156 test_clicked = st.button(
157 f"🔌 {tr('settings.llm.test_connection')}",
158 help=tr("settings.llm.test_connection_help"),
159 key="test_llm_connection_btn",
160 use_container_width=True
161 )
162
163 # Handle load models button click
164 if load_clicked:
165 if llm_api_key and llm_base_url:
166 try:
167 from pixelle_video.utils.llm_util import fetch_available_models
168 with st.spinner(tr("settings.llm.loading_models")):
169 models = fetch_available_models(llm_api_key, llm_base_url)
170 st.session_state.llm_loaded_models = models
171 st.success(tr("settings.llm.models_loaded").replace("{count}", str(len(models))))
172 safe_rerun()
173 except Exception as e:
174 st.error(tr("settings.llm.models_load_failed").replace("{error}", str(e)))
175 else:
176 st.warning(tr("status.llm_config_incomplete"))
177
178 # Handle test connection button click
179 if test_clicked:
180 if llm_api_key and llm_base_url:
181 try:
182 from pixelle_video.utils.llm_util import test_llm_connection
183 with st.spinner(tr("settings.llm.loading_models")):
184 success, message, model_count = test_llm_connection(llm_api_key, llm_base_url)
185 if success:
186 st.success(tr("settings.llm.connection_success").replace("{count}", str(model_count)))
187 else:
188 st.error(tr("settings.llm.connection_failed").replace("{error}", message))
189 except Exception as e:
190 st.error(tr("settings.llm.connection_failed").replace("{error}", str(e)))
191 else:
192 st.warning(tr("status.llm_config_incomplete"))
193
194 # If custom option selected, show text input for custom model name
195 if selected_model_option == CUSTOM_MODEL_OPTION:
196 llm_model = st.text_input(
197 tr("settings.llm.custom_model_input"),
198 value=default_model,
199 help=tr("settings.llm.model_help"),
200 key=f"llm_custom_model_input_{selected_preset}"
201 )
202 else:
203 llm_model = selected_model_option
204
205 # ====================================================================
206 # Column 2: ComfyUI Settings
207 # ====================================================================
208 with comfyui_col:
209 with st.container(border=True):
210 st.markdown(f"**{tr('settings.comfyui.title')}**")
211
212 # Get current configuration
213 comfyui_config = config_manager.get_comfyui_config()
214
215 # Local/Self-hosted ComfyUI configuration
216 st.markdown(f"**{tr('settings.comfyui.local_title')}**")
217 url_col, key_col = st.columns(2)
218 with url_col:
219 comfyui_url = st.text_input(
220 tr("settings.comfyui.comfyui_url"),
221 value=comfyui_config.get("comfyui_url", "http://127.0.0.1:8188"),
222 help=tr("settings.comfyui.comfyui_url_help"),
223 key="comfyui_url_input"
224 )
225 with key_col:
226 comfyui_api_key = st.text_input(
227 tr("settings.comfyui.comfyui_api_key"),
228 value=comfyui_config.get("comfyui_api_key", ""),
229 type="password",
230 help=tr("settings.comfyui.comfyui_api_key_help"),
231 key="comfyui_api_key_input"
232 )
233
234 # Test connection button
235 if st.button(tr("btn.test_connection"), key="test_comfyui", use_container_width=True):
236 try:
237 import requests
238 response = requests.get(f"{comfyui_url}/system_stats", timeout=5)
239 if response.status_code == 200:
240 st.success(tr("status.connection_success"))
241 else:
242 st.error(tr("status.connection_failed"))
243 except Exception as e:
244 st.error(f"{tr('status.connection_failed')}: {str(e)}")
245
246 st.markdown("---")
247
248 # RunningHub cloud configuration
249 st.markdown(f"**{tr('settings.comfyui.cloud_title')}**")
250 runninghub_api_key = st.text_input(
251 tr("settings.comfyui.runninghub_api_key"),
252 value=comfyui_config.get("runninghub_api_key", ""),
253 type="password",
254 help=tr("settings.comfyui.runninghub_api_key_help"),
255 key="runninghub_api_key_input"
256 )
257 st.caption(
258 f"{tr('settings.comfyui.runninghub_hint')} "
259 f"[{tr('settings.comfyui.runninghub_get_api_key')}]"
260 f"(https://www.runninghub{'.cn' if get_language() == 'zh_CN' else '.ai'}/?inviteCode=bozpdlbj)"
261 )
262
263 # RunningHub concurrent limit and instance type (in one row)
264 limit_col, instance_col = st.columns(2)
265 with limit_col:
266 runninghub_concurrent_limit = st.number_input(
267 tr("settings.comfyui.runninghub_concurrent_limit"),
268 min_value=1,
269 max_value=10,
270 value=comfyui_config.get("runninghub_concurrent_limit", 1),
271 help=tr("settings.comfyui.runninghub_concurrent_limit_help"),
272 key="runninghub_concurrent_limit_input"
273 )
274 with instance_col:
275 # Check if instance type is "plus" (48G VRAM enabled)
276 current_instance_type = comfyui_config.get("runninghub_instance_type") or ""
277 is_plus_enabled = current_instance_type == "plus"
278 # Instance type options with i18n
279 instance_options = [
280 tr("settings.comfyui.runninghub_instance_24g"),
281 tr("settings.comfyui.runninghub_instance_48g"),
282 ]
283 runninghub_instance_type_display = st.selectbox(
284 tr("settings.comfyui.runninghub_instance_type"),
285 options=instance_options,
286 index=1 if is_plus_enabled else 0,
287 help=tr("settings.comfyui.runninghub_instance_type_help"),
288 key="runninghub_instance_type_input"
289 )
290 # Convert display value back to actual value
291 runninghub_48g_enabled = runninghub_instance_type_display == tr("settings.comfyui.runninghub_instance_48g")
292
293 # ====================================================================
294 # Direct API media providers
295 # ====================================================================
296 zh = get_language() == "zh_CN"
297 api_cfg = config_manager.get_api_providers_config()
298 common_cfg = api_cfg.get("common", {})
299 openai_cfg = api_cfg.get("openai", {})
300 dashscope_cfg = api_cfg.get("dashscope", {})
301 ark_cfg = api_cfg.get("ark", {})
302 kling_cfg = api_cfg.get("kling", {})
303 default_api_base_urls = {
304 "openai": "https://api.openai.com/v1",
305 "dashscope": "https://dashscope.aliyuncs.com/api/v1",
306 "ark": "https://ark.cn-beijing.volces.com/api/v3",
307 "kling": "https://api-beijing.klingai.com",
308 }
309
310 with st.container(border=True):
311 st.markdown("**🧩 API 媒体模型**" if zh else "**🧩 API Media Models**")
312 st.caption(
313 "用于直连图像/视频模型,不影响上方 LLM 与 ComfyUI/RunningHub 配置。"
314 if zh
315 else "Used for direct image/video model calls. This does not affect the LLM or ComfyUI/RunningHub settings above."
316 )
317
318 common_col, proxy_col = st.columns(2)
319 with common_col:
320 api_print_model_input = st.checkbox(
321 "打印模型请求参数" if zh else "Print model request parameters",
322 value=bool(common_cfg.get("print_model_input", False)),
323 help=(
324 "调试用。开启后会在终端打印发送给图像/视频模型的 prompt、模型名和输入文件路径。"
325 if zh
326 else "For debugging. Prints prompts, model names and input file paths sent to image/video models."
327 ),
328 key="api_media_print_model_input",
329 )
330 with proxy_col:
331 api_local_proxy = st.text_input(
332 "本地代理(可选)" if zh else "Local proxy (optional)",
333 value=common_cfg.get("local_proxy", ""),
334 placeholder="http://127.0.0.1:9090",
335 help=(
336 "仅部分提供商会使用,例如 OpenAI 图像模型。留空表示不使用代理。"
337 if zh
338 else "Only used by some providers, such as OpenAI image models. Leave blank to disable."
339 ),
340 key="api_media_local_proxy",
341 )
342
343 st.markdown("---")
344
345 provider_col1, provider_col2 = st.columns(2)
346 with provider_col1:
347 st.markdown("**OpenAI / GPT Image**")
348 api_openai_use_proxy = st.checkbox(
349 "OpenAI 启用代理" if zh else "Use proxy for OpenAI",
350 value=bool(openai_cfg.get("use_proxy", False)),
351 key="api_media_openai_use_proxy",
352 )
353 api_openai_key = st.text_input(
354 "OpenAI API Key",
355 value=openai_cfg.get("api_key", ""),
356 type="password",
357 key="api_media_openai_key",
358 )
359 api_openai_base_url = st.text_input(
360 "OpenAI Base URL",
361 value=openai_cfg.get("base_url") or default_api_base_urls["openai"],
362 placeholder="https://api.openai.com/v1",
363 key="api_media_openai_base_url",
364 )
365
366 st.markdown("**DashScope / Wan / HappyHorse**")
367 api_dashscope_use_proxy = st.checkbox(
368 "DashScope 启用代理" if zh else "Use proxy for DashScope",
369 value=bool(dashscope_cfg.get("use_proxy", False)),
370 key="api_media_dashscope_use_proxy",
371 )
372 api_dashscope_key = st.text_input(
373 "DashScope API Key",
374 value=dashscope_cfg.get("api_key", ""),
375 type="password",
376 key="api_media_dashscope_key",
377 )
378 api_dashscope_base_url = st.text_input(
379 "DashScope Base URL",
380 value=dashscope_cfg.get("base_url") or default_api_base_urls["dashscope"],
381 placeholder="https://dashscope.aliyuncs.com/api/v1",
382 key="api_media_dashscope_base_url",
383 )
384
385 with provider_col2:
386 st.markdown("**Volcengine ARK / Seedream / Seedance**")
387 api_ark_use_proxy = st.checkbox(
388 "ARK 启用代理" if zh else "Use proxy for ARK",
389 value=bool(ark_cfg.get("use_proxy", False)),
390 key="api_media_ark_use_proxy",
391 )
392 api_ark_key = st.text_input(
393 "ARK API Key",
394 value=ark_cfg.get("api_key", ""),
395 type="password",
396 key="api_media_ark_key",
397 )
398 api_ark_base_url = st.text_input(
399 "ARK Base URL",
400 value=ark_cfg.get("base_url") or default_api_base_urls["ark"],
401 placeholder="https://ark.cn-beijing.volces.com/api/v3",
402 key="api_media_ark_base_url",
403 )
404
405 st.markdown("**Kling AI / 可灵**")
406 api_kling_use_proxy = st.checkbox(
407 "Kling 启用代理" if zh else "Use proxy for Kling",
408 value=bool(kling_cfg.get("use_proxy", False)),
409 key="api_media_kling_use_proxy",
410 )
411 api_kling_base_url = st.text_input(
412 "Kling Base URL",
413 value=kling_cfg.get("base_url") or default_api_base_urls["kling"],
414 placeholder="https://api-beijing.klingai.com",
415 key="api_media_kling_base_url",
416 )
417 api_kling_access_key = st.text_input(
418 "Kling Access Key",
419 value=kling_cfg.get("access_key", ""),
420 type="password",
421 key="api_media_kling_access_key",
422 )
423 api_kling_secret_key = st.text_input(
424 "Kling Secret Key",
425 value=kling_cfg.get("secret_key", ""),
426 type="password",
427 key="api_media_kling_secret_key",
428 )
429
430 # ====================================================================
431 # Action Buttons (full width at bottom)
432 # ====================================================================
433 st.markdown("---")
434
435 col1, col2 = st.columns(2)
436 with col1:
437 if st.button(tr("btn.save_config"), use_container_width=True, key="save_config_btn"):
438 try:
439 # Validate and save LLM configuration
440 if not (llm_api_key and llm_base_url and llm_model):
441 st.error(tr("status.llm_config_incomplete"))
442 else:
443 config_manager.set_llm_config(llm_api_key, llm_base_url, llm_model)
444
445 # Save ComfyUI configuration (optional fields, always save what's provided)
446 # Convert checkbox to instance type: True -> "plus", False -> ""
447 instance_type = "plus" if runninghub_48g_enabled else ""
448 config_manager.set_comfyui_config(
449 comfyui_url=comfyui_url if comfyui_url else None,
450 comfyui_api_key=comfyui_api_key if comfyui_api_key else None,
451 runninghub_api_key=runninghub_api_key if runninghub_api_key else None,
452 runninghub_concurrent_limit=int(runninghub_concurrent_limit),
453 runninghub_instance_type=instance_type
454 )
455
456 # Save direct image/video API provider configuration.
457 config_manager.set_api_provider_config("common", {
458 "print_model_input": bool(api_print_model_input),
459 "local_proxy": api_local_proxy or "",
460 })
461 config_manager.set_api_provider_config("openai", {
462 "api_key": api_openai_key or "",
463 "base_url": api_openai_base_url or "",
464 "use_proxy": bool(api_openai_use_proxy),
465 })
466 config_manager.set_api_provider_config("dashscope", {
467 "api_key": api_dashscope_key or "",
468 "base_url": api_dashscope_base_url or "",
469 "use_proxy": bool(api_dashscope_use_proxy),
470 })
471 config_manager.set_api_provider_config("ark", {
472 "api_key": api_ark_key or "",
473 "base_url": api_ark_base_url or "",
474 "use_proxy": bool(api_ark_use_proxy),
475 })
476 config_manager.set_api_provider_config("kling", {
477 "base_url": api_kling_base_url or "",
478 "access_key": api_kling_access_key or "",
479 "secret_key": api_kling_secret_key or "",
480 "use_proxy": bool(api_kling_use_proxy),
481 })
482
483 # Only save to file if LLM config is valid
484 if llm_api_key and llm_base_url and llm_model:
485 config_manager.save()
486 st.success(tr("status.config_saved"))
487 safe_rerun()
488 except Exception as e:
489 st.error(f"{tr('status.save_failed')}: {str(e)}")
490
491 with col2:
492 if st.button(tr("btn.reset_config"), use_container_width=True, key="reset_config_btn"):
493 # Reset to default
494 from pixelle_video.config.schema import PixelleVideoConfig
495 config_manager.config = PixelleVideoConfig()
496 config_manager.save()
497 st.success(tr("status.config_reset"))
498 safe_rerun()
499
499 lines PYTHON