mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
Merge pull request #597 from willmiao/codex/refactor-settings-manager-for-core-keys
Reduce core settings persistence to essential keys
This commit is contained in:
@@ -149,8 +149,8 @@ Enhance your Civitai browsing experience with our companion browser extension! S
|
||||
### Option 2: **Portable Standalone Edition** (No ComfyUI required)
|
||||
|
||||
1. Download the [Portable Package](https://github.com/willmiao/ComfyUI-Lora-Manager/releases/download/v0.9.8/lora_manager_portable.7z)
|
||||
2. Copy the provided `settings.json.example` file to create a new file named `settings.json` in `comfyui-lora-manager` folder
|
||||
3. Edit the new `settings.json` to include your correct model folder paths and CivitAI API key
|
||||
2. Copy the provided `settings.json.example` file to create a new file named `settings.json` in `comfyui-lora-manager` folder. Only adjust the API key, optional language, and folder paths—the library registry is generated automatically at runtime.
|
||||
3. Edit the new `settings.json` to include your correct model folder paths and CivitAI API key (or keep the placeholders until you are ready to configure them)
|
||||
4. Run run.bat
|
||||
- To change the startup port, edit `run.bat` and modify the parameter (e.g. `--port 9001`)
|
||||
|
||||
@@ -231,8 +231,8 @@ You can now run LoRA Manager independently from ComfyUI:
|
||||
```
|
||||
|
||||
2. **For non-ComfyUI users**:
|
||||
- Copy the provided `settings.json.example` file to create a new file named `settings.json`
|
||||
- Edit `settings.json` to include your correct model folder paths and CivitAI API key
|
||||
- Copy the provided `settings.json.example` file to create a new file named `settings.json`. Update the API key, optional language, and folder paths only—the library registry is created automatically when LoRA Manager starts.
|
||||
- Edit `settings.json` to include your correct model folder paths and CivitAI API key (you can leave the defaults until ready to configure them)
|
||||
- Install required dependencies: `pip install -r requirements.txt`
|
||||
- Run standalone mode:
|
||||
```bash
|
||||
|
||||
@@ -3,6 +3,7 @@ import copy
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from threading import Lock
|
||||
from typing import Any, Awaitable, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
||||
@@ -19,6 +20,12 @@ from ..utils.tag_priorities import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CORE_USER_SETTING_KEYS: Tuple[str, ...] = (
|
||||
"civitai_api_key",
|
||||
"folder_paths",
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"civitai_api_key": "",
|
||||
"language": "en",
|
||||
@@ -35,6 +42,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"default_embedding_root": "",
|
||||
"base_model_path_mappings": {},
|
||||
"download_path_templates": {},
|
||||
"folder_paths": {},
|
||||
"example_images_path": "",
|
||||
"optimize_example_images": True,
|
||||
"auto_download_example_images": False,
|
||||
@@ -57,6 +65,7 @@ class SettingsManager:
|
||||
self._startup_messages: List[Dict[str, Any]] = []
|
||||
self._needs_initial_save = False
|
||||
self._bootstrap_reason: Optional[str] = None
|
||||
self._seed_template: Optional[Dict[str, Any]] = None
|
||||
self.settings = self._load_settings()
|
||||
self._migrate_setting_keys()
|
||||
self._ensure_default_settings()
|
||||
@@ -114,31 +123,99 @@ class SettingsManager:
|
||||
if not os.path.exists(self.settings_file):
|
||||
self._needs_initial_save = True
|
||||
self._bootstrap_reason = "missing"
|
||||
seeded = self._load_settings_template()
|
||||
if seeded is not None:
|
||||
defaults = self._get_default_settings()
|
||||
merged = self._merge_template_with_defaults(defaults, seeded)
|
||||
return merged
|
||||
return self._get_default_settings()
|
||||
|
||||
def _load_settings_template(self) -> Optional[Dict[str, Any]]:
|
||||
"""Load the bundled template when no user settings are found."""
|
||||
|
||||
template_path = Path(__file__).resolve().parents[2] / "settings.json.example"
|
||||
try:
|
||||
with template_path.open("r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
except FileNotFoundError:
|
||||
logger.debug("settings.json.example not found at %s", template_path)
|
||||
return None
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning("Failed to parse settings.json.example: %s", exc)
|
||||
return None
|
||||
|
||||
if not isinstance(data, dict):
|
||||
logger.debug("settings.json.example is not a JSON object; ignoring template")
|
||||
return None
|
||||
|
||||
self._seed_template = copy.deepcopy(data)
|
||||
return copy.deepcopy(data)
|
||||
|
||||
def _merge_template_with_defaults(
|
||||
self, defaults: Dict[str, Any], template: Mapping[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Merge template values into the in-memory defaults."""
|
||||
|
||||
merged = copy.deepcopy(defaults)
|
||||
for key, value in template.items():
|
||||
if key == "folder_paths" and isinstance(value, Mapping):
|
||||
merged[key] = self._normalize_folder_paths(value)
|
||||
else:
|
||||
merged[key] = copy.deepcopy(value)
|
||||
|
||||
merged.setdefault("language", "en")
|
||||
merged.setdefault("folder_paths", {})
|
||||
library_name = merged.get("active_library") or "default"
|
||||
merged["libraries"] = {
|
||||
library_name: self._build_library_payload(
|
||||
folder_paths=merged.get("folder_paths", {}),
|
||||
default_lora_root=merged.get("default_lora_root"),
|
||||
default_checkpoint_root=merged.get("default_checkpoint_root"),
|
||||
default_embedding_root=merged.get("default_embedding_root"),
|
||||
)
|
||||
}
|
||||
merged["active_library"] = library_name
|
||||
return merged
|
||||
|
||||
def _ensure_default_settings(self) -> None:
|
||||
"""Ensure all default settings keys exist"""
|
||||
updated = False
|
||||
normalized_priority = self._normalize_priority_tag_config(
|
||||
self.settings.get("priority_tags")
|
||||
)
|
||||
if normalized_priority != self.settings.get("priority_tags"):
|
||||
self.settings["priority_tags"] = normalized_priority
|
||||
updated = True
|
||||
for key, value in self._get_default_settings().items():
|
||||
defaults = self._get_default_settings()
|
||||
updated_existing = False
|
||||
inserted_defaults = False
|
||||
|
||||
if "priority_tags" in self.settings:
|
||||
normalized_priority = self._normalize_priority_tag_config(
|
||||
self.settings.get("priority_tags")
|
||||
)
|
||||
if normalized_priority != self.settings.get("priority_tags"):
|
||||
self.settings["priority_tags"] = normalized_priority
|
||||
updated_existing = True
|
||||
else:
|
||||
self.settings["priority_tags"] = copy.deepcopy(
|
||||
defaults.get("priority_tags", DEFAULT_PRIORITY_TAG_CONFIG)
|
||||
)
|
||||
inserted_defaults = True
|
||||
|
||||
for key, value in defaults.items():
|
||||
if key == "priority_tags":
|
||||
continue
|
||||
if key not in self.settings:
|
||||
if isinstance(value, dict):
|
||||
self.settings[key] = value.copy()
|
||||
self.settings[key] = copy.deepcopy(value)
|
||||
else:
|
||||
self.settings[key] = value
|
||||
updated = True
|
||||
if updated:
|
||||
inserted_defaults = True
|
||||
|
||||
if updated_existing or (
|
||||
inserted_defaults and self._bootstrap_reason in {"invalid", "unreadable"}
|
||||
):
|
||||
self._save_settings()
|
||||
|
||||
def _migrate_to_library_registry(self) -> None:
|
||||
"""Ensure settings include the multi-library registry structure."""
|
||||
libraries = self.settings.get("libraries")
|
||||
active_name = self.settings.get("active_library")
|
||||
initial_bootstrap = self._bootstrap_reason == "missing"
|
||||
|
||||
if not isinstance(libraries, dict) or not libraries:
|
||||
library_name = active_name or "default"
|
||||
@@ -152,7 +229,8 @@ class SettingsManager:
|
||||
self.settings["libraries"] = libraries
|
||||
self.settings["active_library"] = library_name
|
||||
self._sync_active_library_to_root(save=False)
|
||||
self._save_settings()
|
||||
if not initial_bootstrap:
|
||||
self._save_settings()
|
||||
return
|
||||
|
||||
sanitized_libraries: Dict[str, Dict[str, Any]] = {}
|
||||
@@ -177,12 +255,15 @@ class SettingsManager:
|
||||
self.settings["libraries"] = sanitized_libraries
|
||||
|
||||
if not active_name or active_name not in sanitized_libraries:
|
||||
changed = True
|
||||
if sanitized_libraries:
|
||||
self.settings["active_library"] = next(iter(sanitized_libraries.keys()))
|
||||
else:
|
||||
self.settings["active_library"] = "default"
|
||||
|
||||
self._sync_active_library_to_root(save=changed)
|
||||
self._sync_active_library_to_root(save=changed and not initial_bootstrap)
|
||||
if changed and initial_bootstrap:
|
||||
self._needs_initial_save = True
|
||||
|
||||
def _sync_active_library_to_root(self, *, save: bool = False) -> None:
|
||||
"""Update top-level folder path settings to mirror the active library."""
|
||||
@@ -427,7 +508,10 @@ class SettingsManager:
|
||||
default_checkpoint_root=self.settings.get('default_checkpoint_root'),
|
||||
default_embedding_root=self.settings.get('default_embedding_root'),
|
||||
)
|
||||
self._save_settings()
|
||||
if self._bootstrap_reason == "missing":
|
||||
self._needs_initial_save = True
|
||||
else:
|
||||
self._save_settings()
|
||||
|
||||
def _check_environment_variables(self) -> None:
|
||||
"""Check for environment variables and update settings if needed"""
|
||||
@@ -524,11 +608,21 @@ class SettingsManager:
|
||||
|
||||
def _get_default_settings(self) -> Dict[str, Any]:
|
||||
"""Return default settings"""
|
||||
defaults = DEFAULT_SETTINGS.copy()
|
||||
# Ensure nested dicts are independent copies
|
||||
defaults = copy.deepcopy(DEFAULT_SETTINGS)
|
||||
defaults['base_model_path_mappings'] = {}
|
||||
defaults['download_path_templates'] = {}
|
||||
defaults['priority_tags'] = DEFAULT_PRIORITY_TAG_CONFIG.copy()
|
||||
defaults.setdefault('folder_paths', {})
|
||||
|
||||
library_name = defaults.get("active_library") or "default"
|
||||
default_library = self._build_library_payload(
|
||||
folder_paths=defaults.get("folder_paths", {}),
|
||||
default_lora_root=defaults.get("default_lora_root"),
|
||||
default_checkpoint_root=defaults.get("default_checkpoint_root"),
|
||||
default_embedding_root=defaults.get("default_embedding_root"),
|
||||
)
|
||||
defaults['libraries'] = {library_name: default_library}
|
||||
defaults['active_library'] = library_name
|
||||
return defaults
|
||||
|
||||
def _normalize_priority_tag_config(self, value: Any) -> Dict[str, str]:
|
||||
@@ -685,10 +779,32 @@ class SettingsManager:
|
||||
def _save_settings(self) -> None:
|
||||
"""Save settings to file"""
|
||||
try:
|
||||
payload = self._serialize_settings_for_disk()
|
||||
with open(self.settings_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.settings, f, indent=2)
|
||||
json.dump(payload, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving settings: {e}")
|
||||
else:
|
||||
if self._bootstrap_reason == "missing":
|
||||
self._bootstrap_reason = None
|
||||
self._seed_template = None
|
||||
|
||||
def _serialize_settings_for_disk(self) -> Dict[str, Any]:
|
||||
"""Return the settings payload that should be persisted to disk."""
|
||||
|
||||
if self._bootstrap_reason == "missing":
|
||||
minimal: Dict[str, Any] = {}
|
||||
for key in CORE_USER_SETTING_KEYS:
|
||||
if key in self.settings:
|
||||
minimal[key] = copy.deepcopy(self.settings[key])
|
||||
|
||||
if self._seed_template:
|
||||
for key, value in self._seed_template.items():
|
||||
minimal.setdefault(key, copy.deepcopy(value))
|
||||
|
||||
return minimal
|
||||
|
||||
return copy.deepcopy(self.settings)
|
||||
|
||||
def get_libraries(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Return a copy of the registered libraries."""
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
{
|
||||
"_note": "LoRA Manager builds the detailed library registry automatically at runtime.",
|
||||
"language": "en",
|
||||
"civitai_api_key": "your_civitai_api_key_here",
|
||||
"folder_paths": {
|
||||
"loras": [
|
||||
"C:/path/to/your/loras_folder",
|
||||
"C:/path/to/another/loras_folder"
|
||||
"C:/path/to/your/loras_folder"
|
||||
],
|
||||
"checkpoints": [
|
||||
"C:/path/to/your/checkpoints_folder",
|
||||
"C:/path/to/another/checkpoints_folder"
|
||||
"C:/path/to/your/checkpoints_folder"
|
||||
],
|
||||
"embeddings": [
|
||||
"C:/path/to/your/embeddings_folder",
|
||||
"C:/path/to/another/embeddings_folder"
|
||||
"C:/path/to/your/embeddings_folder"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,35 @@ def manager(tmp_path, monkeypatch):
|
||||
return mgr
|
||||
|
||||
|
||||
def test_initial_save_persists_minimal_template(tmp_path, monkeypatch):
|
||||
settings_path = tmp_path / "settings.json"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.settings_manager.ensure_settings_file",
|
||||
lambda logger=None: str(settings_path),
|
||||
)
|
||||
|
||||
template = {
|
||||
"_note": "template note",
|
||||
"language": "fr",
|
||||
"folder_paths": {"loras": ["/loras"]},
|
||||
}
|
||||
|
||||
def fake_template_loader(self):
|
||||
self._seed_template = copy.deepcopy(template)
|
||||
return copy.deepcopy(template)
|
||||
|
||||
monkeypatch.setattr(SettingsManager, "_load_settings_template", fake_template_loader)
|
||||
|
||||
manager = SettingsManager()
|
||||
|
||||
persisted = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||
assert persisted["_note"] == "template note"
|
||||
assert "libraries" not in persisted
|
||||
assert persisted["folder_paths"]["loras"] == ["/loras"]
|
||||
assert manager.get_libraries()["default"]["folder_paths"]["loras"] == ["/loras"]
|
||||
|
||||
|
||||
def test_environment_variable_overrides_settings(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None)
|
||||
monkeypatch.setenv("CIVITAI_API_KEY", "secret")
|
||||
|
||||
Reference in New Issue
Block a user