From e0332571da3a5d8eec6ebbb185f7caef4a53df28 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sun, 26 Oct 2025 23:40:07 +0800 Subject: [PATCH] feat(settings): improve library bootstrap logic and path handling - Normalize folder paths before library bootstrap to ensure consistent structure - Add _has_configured_paths helper to detect valid folder configurations - Enhance bootstrap logic to handle edge cases with single libraries and empty paths - Update library payload construction to use normalized paths - Add example settings file changes to demonstrate new path structure The changes ensure more robust library initialization when folder paths are configured at the top level but not properly propagated to individual libraries. --- py/services/settings_manager.py | 69 +++++++++++++++++++++++-- settings.json.example | 11 ++-- tests/services/test_settings_manager.py | 39 ++++++++++++++ 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 467959fb..2ececf6e 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -217,10 +217,32 @@ class SettingsManager: active_name = self.settings.get("active_library") initial_bootstrap = self._bootstrap_reason == "missing" - if not isinstance(libraries, dict) or not libraries: + raw_top_level_paths = self.settings.get("folder_paths", {}) + normalized_top_level_paths: Dict[str, List[str]] = {} + if isinstance(raw_top_level_paths, Mapping): + normalized_top_level_paths = self._normalize_folder_paths(raw_top_level_paths) + if normalized_top_level_paths != raw_top_level_paths: + self.settings["folder_paths"] = copy.deepcopy(normalized_top_level_paths) + + top_level_has_paths = self._has_configured_paths(normalized_top_level_paths) + + needs_library_bootstrap = not isinstance(libraries, dict) or not libraries + + if ( + not needs_library_bootstrap + and top_level_has_paths + and len(libraries) == 1 + ): + only_library_payload = next(iter(libraries.values())) + if isinstance(only_library_payload, Mapping): + folder_payload = only_library_payload.get("folder_paths") + if not self._has_configured_paths(folder_payload): + needs_library_bootstrap = True + + if needs_library_bootstrap: library_name = active_name or "default" library_payload = self._build_library_payload( - folder_paths=self.settings.get("folder_paths", {}), + folder_paths=normalized_top_level_paths, default_lora_root=self.settings.get("default_lora_root", ""), default_checkpoint_root=self.settings.get("default_checkpoint_root", ""), default_embedding_root=self.settings.get("default_embedding_root", ""), @@ -233,14 +255,36 @@ class SettingsManager: self._save_settings() return + seed_library_name: Optional[str] = None + if top_level_has_paths and isinstance(libraries, dict): + target_name: Optional[str] = None + if active_name and active_name in libraries: + target_name = active_name + elif len(libraries) == 1: + target_name = next(iter(libraries.keys())) + + if target_name: + candidate_payload = libraries.get(target_name) + if isinstance(candidate_payload, Mapping) and not self._has_configured_paths(candidate_payload.get("folder_paths")): + seed_library_name = target_name + sanitized_libraries: Dict[str, Dict[str, Any]] = {} changed = False for name, data in libraries.items(): if not isinstance(data, dict): data = {} changed = True + + candidate_folder_paths = data.get("folder_paths") + if ( + seed_library_name == name + and not self._has_configured_paths(candidate_folder_paths) + and top_level_has_paths + ): + candidate_folder_paths = normalized_top_level_paths + payload = self._build_library_payload( - folder_paths=data.get("folder_paths"), + folder_paths=candidate_folder_paths, default_lora_root=data.get("default_lora_root"), default_checkpoint_root=data.get("default_checkpoint_root"), default_embedding_root=data.get("default_embedding_root"), @@ -352,6 +396,25 @@ class SettingsManager: normalized[key] = cleaned return normalized + def _has_configured_paths(self, folder_paths: Any) -> bool: + if not isinstance(folder_paths, Mapping): + return False + + for values in folder_paths.values(): + if isinstance(values, str): + candidate_values = [values] + else: + try: + candidate_values = list(values) # type: ignore[arg-type] + except TypeError: + continue + + for path in candidate_values: + if isinstance(path, str) and path.strip(): + return True + + return False + def _validate_folder_paths( self, library_name: str, diff --git a/settings.json.example b/settings.json.example index b5ea2e0a..673aa76d 100644 --- a/settings.json.example +++ b/settings.json.example @@ -1,16 +1,17 @@ { - "_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/your/loras_folder", + "C:/path/to/another/loras_folder" ], "checkpoints": [ - "C:/path/to/your/checkpoints_folder" + "C:/path/to/your/checkpoints_folder", + "C:/path/to/another/checkpoints_folder" ], "embeddings": [ - "C:/path/to/your/embeddings_folder" + "C:/path/to/your/embeddings_folder", + "C:/path/to/another/embeddings_folder" ] } } diff --git a/tests/services/test_settings_manager.py b/tests/services/test_settings_manager.py index 71447baf..1643f24f 100644 --- a/tests/services/test_settings_manager.py +++ b/tests/services/test_settings_manager.py @@ -53,6 +53,45 @@ def test_initial_save_persists_minimal_template(tmp_path, monkeypatch): assert manager.get_libraries()["default"]["folder_paths"]["loras"] == ["/loras"] +def test_existing_folder_paths_seed_default_library(tmp_path, monkeypatch): + monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1") + + lora_dir = tmp_path / "loras" + checkpoint_dir = tmp_path / "checkpoints" + unet_dir = tmp_path / "unet" + diffusion_dir = tmp_path / "diffusion_models" + embedding_dir = tmp_path / "embeddings" + + for directory in (lora_dir, checkpoint_dir, unet_dir, diffusion_dir, embedding_dir): + directory.mkdir() + + initial = { + "folder_paths": { + "loras": [str(lora_dir)], + "checkpoints": [str(checkpoint_dir)], + "unet": [str(diffusion_dir), str(unet_dir)], + "embeddings": [str(embedding_dir)], + } + } + + manager = _create_manager_with_settings(tmp_path, monkeypatch, initial) + + stored_paths = manager.get("folder_paths") + assert stored_paths["loras"] == [str(lora_dir)] + assert stored_paths["checkpoints"] == [str(checkpoint_dir)] + assert stored_paths["unet"] == [str(diffusion_dir), str(unet_dir)] + assert stored_paths["embeddings"] == [str(embedding_dir)] + + libraries = manager.get_libraries() + assert "default" in libraries + assert libraries["default"]["folder_paths"]["loras"] == [str(lora_dir)] + assert libraries["default"]["folder_paths"]["checkpoints"] == [str(checkpoint_dir)] + assert libraries["default"]["folder_paths"]["unet"] == [str(diffusion_dir), str(unet_dir)] + assert libraries["default"]["folder_paths"]["embeddings"] == [str(embedding_dir)] + + assert manager.get_startup_messages() == [] + + def test_environment_variable_overrides_settings(tmp_path, monkeypatch): monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None) monkeypatch.setenv("CIVITAI_API_KEY", "secret")