settings.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. System settings component for web UI
  14. """
  15. import streamlit as st
  16. from web.i18n import tr, get_language
  17. from web.utils.streamlit_helpers import safe_rerun
  18. from pixelle_video.config import config_manager
  19. def render_advanced_settings():
  20. """Render system configuration (required) with 2-column layout"""
  21. # Check if system is configured
  22. is_configured = config_manager.validate()
  23. # Expand if not configured, collapse if configured
  24. with st.expander(tr("settings.title"), expanded=not is_configured):
  25. # 2-column layout: LLM | ComfyUI
  26. llm_col, comfyui_col = st.columns(2)
  27. # ====================================================================
  28. # Column 1: LLM Settings
  29. # ====================================================================
  30. with llm_col:
  31. with st.container(border=True):
  32. st.markdown(f"**{tr('settings.llm.title')}**")
  33. # Quick preset selection
  34. from pixelle_video.llm_presets import get_preset_names, get_preset, find_preset_by_base_url_and_model
  35. # Custom at the end
  36. preset_names = get_preset_names() + ["Custom"]
  37. # Get current config
  38. current_llm = config_manager.get_llm_config()
  39. # Auto-detect which preset matches current config
  40. current_preset = find_preset_by_base_url_and_model(
  41. current_llm["base_url"],
  42. current_llm["model"]
  43. )
  44. # Determine default index based on current config
  45. if current_preset:
  46. # Current config matches a preset
  47. default_index = preset_names.index(current_preset)
  48. else:
  49. # Current config doesn't match any preset -> Custom
  50. default_index = len(preset_names) - 1
  51. selected_preset = st.selectbox(
  52. tr("settings.llm.quick_select"),
  53. options=preset_names,
  54. index=default_index,
  55. help=tr("settings.llm.quick_select_help"),
  56. key="llm_preset_select"
  57. )
  58. # Auto-fill based on selected preset
  59. if selected_preset != "Custom":
  60. # Preset selected
  61. preset_config = get_preset(selected_preset)
  62. # If user switched to a different preset (not current one), clear API key
  63. # If it's the same as current config, keep API key
  64. if selected_preset == current_preset:
  65. # Same preset as saved config: keep API key
  66. default_api_key = current_llm["api_key"]
  67. else:
  68. # Different preset: use default_api_key if provided (e.g., Ollama), otherwise clear
  69. default_api_key = preset_config.get("default_api_key", "")
  70. default_base_url = preset_config.get("base_url", "")
  71. default_model = preset_config.get("model", "")
  72. # Show API key URL if available
  73. if preset_config.get("api_key_url"):
  74. st.markdown(f"🔑 [{tr('settings.llm.get_api_key')}]({preset_config['api_key_url']})")
  75. else:
  76. # Custom: show current saved config (if any)
  77. default_api_key = current_llm["api_key"]
  78. default_base_url = current_llm["base_url"]
  79. default_model = current_llm["model"]
  80. st.markdown("---")
  81. # API Key (use unique key to force refresh when switching preset)
  82. llm_api_key = st.text_input(
  83. f"{tr('settings.llm.api_key')} *",
  84. value=default_api_key,
  85. type="password",
  86. help=tr("settings.llm.api_key_help"),
  87. key=f"llm_api_key_input_{selected_preset}"
  88. )
  89. # Base URL (use unique key based on preset to force refresh)
  90. llm_base_url = st.text_input(
  91. f"{tr('settings.llm.base_url')} *",
  92. value=default_base_url,
  93. help=tr("settings.llm.base_url_help"),
  94. key=f"llm_base_url_input_{selected_preset}"
  95. )
  96. # Model selection with dropdown and load button
  97. # Initialize session state for loaded models
  98. if "llm_loaded_models" not in st.session_state:
  99. st.session_state.llm_loaded_models = []
  100. # Build model options: Custom option + loaded models
  101. CUSTOM_MODEL_OPTION = f"✏️ {tr('settings.llm.custom_model')}"
  102. model_options = [CUSTOM_MODEL_OPTION] + st.session_state.llm_loaded_models
  103. # Determine default selection
  104. if default_model in st.session_state.llm_loaded_models:
  105. default_model_index = model_options.index(default_model)
  106. else:
  107. # Default model not in loaded list, use custom
  108. default_model_index = 0
  109. # Model dropdown with load button on the right
  110. model_col, load_col, test_col = st.columns([3, 1, 1])
  111. with model_col:
  112. selected_model_option = st.selectbox(
  113. f"{tr('settings.llm.model')} *",
  114. options=model_options,
  115. index=default_model_index,
  116. help=tr("settings.llm.model_help"),
  117. key=f"llm_model_select_{selected_preset}"
  118. )
  119. with load_col:
  120. st.markdown("<div style='height: 28px'></div>", unsafe_allow_html=True)
  121. load_clicked = st.button(
  122. f"🔄 {tr('settings.llm.load_models')}",
  123. help=tr("settings.llm.load_models_help"),
  124. key="load_models_btn",
  125. use_container_width=True
  126. )
  127. with test_col:
  128. st.markdown("<div style='height: 28px'></div>", unsafe_allow_html=True)
  129. test_clicked = st.button(
  130. f"🔌 {tr('settings.llm.test_connection')}",
  131. help=tr("settings.llm.test_connection_help"),
  132. key="test_llm_connection_btn",
  133. use_container_width=True
  134. )
  135. # Handle load models button click
  136. if load_clicked:
  137. if llm_api_key and llm_base_url:
  138. try:
  139. from pixelle_video.utils.llm_util import fetch_available_models
  140. with st.spinner(tr("settings.llm.loading_models")):
  141. models = fetch_available_models(llm_api_key, llm_base_url)
  142. st.session_state.llm_loaded_models = models
  143. st.success(tr("settings.llm.models_loaded").replace("{count}", str(len(models))))
  144. safe_rerun()
  145. except Exception as e:
  146. st.error(tr("settings.llm.models_load_failed").replace("{error}", str(e)))
  147. else:
  148. st.warning(tr("status.llm_config_incomplete"))
  149. # Handle test connection button click
  150. if test_clicked:
  151. if llm_api_key and llm_base_url:
  152. try:
  153. from pixelle_video.utils.llm_util import test_llm_connection
  154. with st.spinner(tr("settings.llm.loading_models")):
  155. success, message, model_count = test_llm_connection(llm_api_key, llm_base_url)
  156. if success:
  157. st.success(tr("settings.llm.connection_success").replace("{count}", str(model_count)))
  158. else:
  159. st.error(tr("settings.llm.connection_failed").replace("{error}", message))
  160. except Exception as e:
  161. st.error(tr("settings.llm.connection_failed").replace("{error}", str(e)))
  162. else:
  163. st.warning(tr("status.llm_config_incomplete"))
  164. # If custom option selected, show text input for custom model name
  165. if selected_model_option == CUSTOM_MODEL_OPTION:
  166. llm_model = st.text_input(
  167. tr("settings.llm.custom_model_input"),
  168. value=default_model,
  169. help=tr("settings.llm.model_help"),
  170. key=f"llm_custom_model_input_{selected_preset}"
  171. )
  172. else:
  173. llm_model = selected_model_option
  174. # ====================================================================
  175. # Column 2: ComfyUI Settings
  176. # ====================================================================
  177. with comfyui_col:
  178. with st.container(border=True):
  179. st.markdown(f"**{tr('settings.comfyui.title')}**")
  180. # Get current configuration
  181. comfyui_config = config_manager.get_comfyui_config()
  182. # Local/Self-hosted ComfyUI configuration
  183. st.markdown(f"**{tr('settings.comfyui.local_title')}**")
  184. url_col, key_col = st.columns(2)
  185. with url_col:
  186. comfyui_url = st.text_input(
  187. tr("settings.comfyui.comfyui_url"),
  188. value=comfyui_config.get("comfyui_url", "http://127.0.0.1:8188"),
  189. help=tr("settings.comfyui.comfyui_url_help"),
  190. key="comfyui_url_input"
  191. )
  192. with key_col:
  193. comfyui_api_key = st.text_input(
  194. tr("settings.comfyui.comfyui_api_key"),
  195. value=comfyui_config.get("comfyui_api_key", ""),
  196. type="password",
  197. help=tr("settings.comfyui.comfyui_api_key_help"),
  198. key="comfyui_api_key_input"
  199. )
  200. # Test connection button
  201. if st.button(tr("btn.test_connection"), key="test_comfyui", use_container_width=True):
  202. try:
  203. import requests
  204. response = requests.get(f"{comfyui_url}/system_stats", timeout=5)
  205. if response.status_code == 200:
  206. st.success(tr("status.connection_success"))
  207. else:
  208. st.error(tr("status.connection_failed"))
  209. except Exception as e:
  210. st.error(f"{tr('status.connection_failed')}: {str(e)}")
  211. st.markdown("---")
  212. # RunningHub cloud configuration
  213. st.markdown(f"**{tr('settings.comfyui.cloud_title')}**")
  214. runninghub_api_key = st.text_input(
  215. tr("settings.comfyui.runninghub_api_key"),
  216. value=comfyui_config.get("runninghub_api_key", ""),
  217. type="password",
  218. help=tr("settings.comfyui.runninghub_api_key_help"),
  219. key="runninghub_api_key_input"
  220. )
  221. st.caption(
  222. f"{tr('settings.comfyui.runninghub_hint')} "
  223. f"[{tr('settings.comfyui.runninghub_get_api_key')}]"
  224. f"(https://www.runninghub{'.cn' if get_language() == 'zh_CN' else '.ai'}/?inviteCode=bozpdlbj)"
  225. )
  226. # RunningHub concurrent limit and instance type (in one row)
  227. limit_col, instance_col = st.columns(2)
  228. with limit_col:
  229. runninghub_concurrent_limit = st.number_input(
  230. tr("settings.comfyui.runninghub_concurrent_limit"),
  231. min_value=1,
  232. max_value=10,
  233. value=comfyui_config.get("runninghub_concurrent_limit", 1),
  234. help=tr("settings.comfyui.runninghub_concurrent_limit_help"),
  235. key="runninghub_concurrent_limit_input"
  236. )
  237. with instance_col:
  238. # Check if instance type is "plus" (48G VRAM enabled)
  239. current_instance_type = comfyui_config.get("runninghub_instance_type") or ""
  240. is_plus_enabled = current_instance_type == "plus"
  241. # Instance type options with i18n
  242. instance_options = [
  243. tr("settings.comfyui.runninghub_instance_24g"),
  244. tr("settings.comfyui.runninghub_instance_48g"),
  245. ]
  246. runninghub_instance_type_display = st.selectbox(
  247. tr("settings.comfyui.runninghub_instance_type"),
  248. options=instance_options,
  249. index=1 if is_plus_enabled else 0,
  250. help=tr("settings.comfyui.runninghub_instance_type_help"),
  251. key="runninghub_instance_type_input"
  252. )
  253. # Convert display value back to actual value
  254. runninghub_48g_enabled = runninghub_instance_type_display == tr("settings.comfyui.runninghub_instance_48g")
  255. # ====================================================================
  256. # Action Buttons (full width at bottom)
  257. # ====================================================================
  258. st.markdown("---")
  259. col1, col2 = st.columns(2)
  260. with col1:
  261. if st.button(tr("btn.save_config"), use_container_width=True, key="save_config_btn"):
  262. try:
  263. # Validate and save LLM configuration
  264. if not (llm_api_key and llm_base_url and llm_model):
  265. st.error(tr("status.llm_config_incomplete"))
  266. else:
  267. config_manager.set_llm_config(llm_api_key, llm_base_url, llm_model)
  268. # Save ComfyUI configuration (optional fields, always save what's provided)
  269. # Convert checkbox to instance type: True -> "plus", False -> ""
  270. instance_type = "plus" if runninghub_48g_enabled else ""
  271. config_manager.set_comfyui_config(
  272. comfyui_url=comfyui_url if comfyui_url else None,
  273. comfyui_api_key=comfyui_api_key if comfyui_api_key else None,
  274. runninghub_api_key=runninghub_api_key if runninghub_api_key else None,
  275. runninghub_concurrent_limit=int(runninghub_concurrent_limit),
  276. runninghub_instance_type=instance_type
  277. )
  278. # Only save to file if LLM config is valid
  279. if llm_api_key and llm_base_url and llm_model:
  280. config_manager.save()
  281. st.success(tr("status.config_saved"))
  282. safe_rerun()
  283. except Exception as e:
  284. st.error(f"{tr('status.save_failed')}: {str(e)}")
  285. with col2:
  286. if st.button(tr("btn.reset_config"), use_container_width=True, key="reset_config_btn"):
  287. # Reset to default
  288. from pixelle_video.config.schema import PixelleVideoConfig
  289. config_manager.config = PixelleVideoConfig()
  290. config_manager.save()
  291. st.success(tr("status.config_reset"))
  292. safe_rerun()