mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-14 19:17:32 +08:00
Merge f334f2db3d into 20f5e474da
This commit is contained in:
commit
d8b9b41f94
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ __pycache__/
|
|||||||
/custom_nodes/
|
/custom_nodes/
|
||||||
!custom_nodes/example_node.py.example
|
!custom_nodes/example_node.py.example
|
||||||
extra_model_paths.yaml
|
extra_model_paths.yaml
|
||||||
|
extra_paths.yaml
|
||||||
/.vs
|
/.vs
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
#Rename this to extra_model_paths.yaml and ComfyUI will load it
|
#Rename this to extra_model_paths.yaml and ComfyUI will load it
|
||||||
|
#
|
||||||
|
# DEPRECATED: extra_model_paths.yaml is superseded by extra_paths.yaml, which supports
|
||||||
|
# all path configuration (system dirs, custom_nodes, and models) in a cleaner format.
|
||||||
|
# See extra_paths.yaml.example. This file continues to work for backward compatibility.
|
||||||
|
|
||||||
#config for comfyui
|
#config for comfyui
|
||||||
#your base path should be either an existing comfy install or a central folder where you store all of your models, loras, etc.
|
#your base path should be either an existing comfy install or a central folder where you store all of your models, loras, etc.
|
||||||
|
|
||||||
|
# When base_path is set, any standard subdirectory that exists on disk is automatically
|
||||||
|
# registered — the explicit paths below are optional and only needed to override a
|
||||||
|
# category or point it to a non-standard location.
|
||||||
|
|
||||||
#comfyui:
|
#comfyui:
|
||||||
# base_path: path/to/comfyui/
|
# base_path: path/to/comfyui/
|
||||||
# # You can use is_default to mark that these folders should be listed first, and used as the default dirs for eg downloads
|
# # You can use is_default to mark that these folders should be listed first, and used as the default dirs for eg downloads
|
||||||
|
|||||||
84
extra_paths.yaml.example
Normal file
84
extra_paths.yaml.example
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#Rename this to extra_paths.yaml and ComfyUI will load it
|
||||||
|
#This is the successor to extra_model_paths.yaml and supports all path configuration in one file.
|
||||||
|
|
||||||
|
#config for comfyui
|
||||||
|
#Set base_path to your ComfyUI install root. System directories (output, input, temp, user)
|
||||||
|
#and custom_nodes are resolved relative to base_path.
|
||||||
|
#
|
||||||
|
#Model paths go under the 'models' block. If you only set models/base_path, all standard
|
||||||
|
#subdirectories that exist on disk are automatically registered — no need to list them.
|
||||||
|
#Explicit paths under models/ are optional and only needed to override a specific category
|
||||||
|
#or point it to a non-standard location.
|
||||||
|
|
||||||
|
#comfyui:
|
||||||
|
# base_path: path/to/comfyui/
|
||||||
|
# # System directories (relative to base_path, or absolute)
|
||||||
|
# output: output/
|
||||||
|
# input: input/
|
||||||
|
# temp: temp/
|
||||||
|
# user: user/
|
||||||
|
# # Custom nodes directory (not auto-scanned; explicit only)
|
||||||
|
# custom_nodes: custom_nodes/
|
||||||
|
# models:
|
||||||
|
# base_path: models/
|
||||||
|
# # You can use is_default to mark that these folders should be listed first,
|
||||||
|
# # and used as the default dirs for eg downloads
|
||||||
|
# #is_default: true
|
||||||
|
# checkpoints: checkpoints/
|
||||||
|
# text_encoders: |
|
||||||
|
# text_encoders/
|
||||||
|
# clip/ # legacy location still supported
|
||||||
|
# clip_vision: clip_vision/
|
||||||
|
# configs: configs/
|
||||||
|
# controlnet: |
|
||||||
|
# controlnet/
|
||||||
|
# t2i_adapter/
|
||||||
|
# diffusion_models: |
|
||||||
|
# diffusion_models/
|
||||||
|
# unet/
|
||||||
|
# diffusers: diffusers/
|
||||||
|
# embeddings: embeddings/
|
||||||
|
# frame_interpolation: frame_interpolation/
|
||||||
|
# gligen: gligen/
|
||||||
|
# hypernetworks: hypernetworks/
|
||||||
|
# latent_upscale_models: latent_upscale_models/
|
||||||
|
# loras: loras/
|
||||||
|
# model_patches: model_patches/
|
||||||
|
# photomaker: photomaker/
|
||||||
|
# style_models: style_models/
|
||||||
|
# upscale_models: upscale_models/
|
||||||
|
# vae: vae/
|
||||||
|
# vae_approx: vae_approx/
|
||||||
|
# audio_encoders: audio_encoders/
|
||||||
|
# classifiers: classifiers/
|
||||||
|
|
||||||
|
|
||||||
|
#config for a1111 ui
|
||||||
|
#all you have to do is uncomment this (remove the #) and change the base_path to where yours is installed
|
||||||
|
|
||||||
|
#a111:
|
||||||
|
# models:
|
||||||
|
# base_path: path/to/stable-diffusion-webui/
|
||||||
|
# checkpoints: models/Stable-diffusion
|
||||||
|
# configs: models/Stable-diffusion
|
||||||
|
# vae: models/VAE
|
||||||
|
# loras: |
|
||||||
|
# models/Lora
|
||||||
|
# models/LyCORIS
|
||||||
|
# upscale_models: |
|
||||||
|
# models/ESRGAN
|
||||||
|
# models/RealESRGAN
|
||||||
|
# models/SwinIR
|
||||||
|
# embeddings: embeddings
|
||||||
|
# hypernetworks: models/hypernetworks
|
||||||
|
# controlnet: models/ControlNet
|
||||||
|
|
||||||
|
|
||||||
|
# For a full list of supported model category keys (style_models, vae_approx, hypernetworks,
|
||||||
|
# photomaker, model_patches, audio_encoders, classifiers, etc.) see folder_paths.py.
|
||||||
|
|
||||||
|
#other_ui:
|
||||||
|
# models:
|
||||||
|
# base_path: path/to/ui
|
||||||
|
# checkpoints: models/checkpoints
|
||||||
|
# gligen: models/gligen
|
||||||
18
main.py
18
main.py
@ -101,9 +101,21 @@ if args.enable_manager:
|
|||||||
|
|
||||||
|
|
||||||
def apply_custom_paths():
|
def apply_custom_paths():
|
||||||
# extra model paths
|
install_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
extra_model_paths_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "extra_model_paths.yaml")
|
|
||||||
if os.path.isfile(extra_model_paths_config_path):
|
# extra_paths.yaml — primary config (superset of extra_model_paths.yaml)
|
||||||
|
extra_paths_config_path = os.path.join(install_dir, "extra_paths.yaml")
|
||||||
|
extra_model_paths_config_path = os.path.join(install_dir, "extra_model_paths.yaml")
|
||||||
|
|
||||||
|
if os.path.isfile(extra_paths_config_path):
|
||||||
|
utils.extra_config.load_extra_path_config(extra_paths_config_path, allow_system_dirs=True)
|
||||||
|
if os.path.isfile(extra_model_paths_config_path):
|
||||||
|
logging.warning(
|
||||||
|
"Both extra_paths.yaml and extra_model_paths.yaml found; "
|
||||||
|
"ignoring the deprecated extra_model_paths.yaml. "
|
||||||
|
"Please remove or migrate its entries to extra_paths.yaml."
|
||||||
|
)
|
||||||
|
elif os.path.isfile(extra_model_paths_config_path):
|
||||||
utils.extra_config.load_extra_path_config(extra_model_paths_config_path)
|
utils.extra_config.load_extra_path_config(extra_model_paths_config_path)
|
||||||
|
|
||||||
if args.extra_model_paths_config:
|
if args.extra_model_paths_config:
|
||||||
|
|||||||
@ -301,3 +301,320 @@ def test_load_extra_path_config_no_base_path(
|
|||||||
actual_diffusion = folder_paths.folder_names_and_paths["diffusion_models"][0]
|
actual_diffusion = folder_paths.folder_names_and_paths["diffusion_models"][0]
|
||||||
assert len(actual_diffusion) == 1, "Should have one path for 'diffusion_models'."
|
assert len(actual_diffusion) == 1, "Should have one path for 'diffusion_models'."
|
||||||
assert actual_diffusion[0] == os.path.abspath(expected_unet)
|
assert actual_diffusion[0] == os.path.abspath(expected_unet)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_load_extra_path_config_implicit_subdirs(
|
||||||
|
mock_yaml_load, clear_folder_paths, tmp_path
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
When base_path is set and no explicit sub-paths are declared, any subdir
|
||||||
|
whose name matches a known category is auto-registered.
|
||||||
|
"""
|
||||||
|
# Create real subdirs that match known categories
|
||||||
|
(tmp_path / "checkpoints").mkdir()
|
||||||
|
(tmp_path / "loras").mkdir()
|
||||||
|
(tmp_path / "unknown_dir").mkdir() # not a registered category — should be ignored
|
||||||
|
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
|
||||||
|
# Pre-populate only the categories we're testing so clear_folder_paths doesn't hide them
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"] = ([], set())
|
||||||
|
folder_paths.folder_names_and_paths["loras"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_model_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("") # content ignored; yaml.safe_load is mocked
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
assert str(tmp_path / "checkpoints") in folder_paths.folder_names_and_paths["checkpoints"][0]
|
||||||
|
assert str(tmp_path / "loras") in folder_paths.folder_names_and_paths["loras"][0]
|
||||||
|
assert "unknown_dir" not in folder_paths.folder_names_and_paths
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_implicit_scan_excludes_custom_nodes(
|
||||||
|
mock_yaml_load, clear_folder_paths, tmp_path
|
||||||
|
):
|
||||||
|
"""custom_nodes must never be auto-registered by the implicit scan."""
|
||||||
|
(tmp_path / "custom_nodes").mkdir()
|
||||||
|
(tmp_path / "checkpoints").mkdir()
|
||||||
|
|
||||||
|
config_data = {"comfyui": {"base_path": str(tmp_path)}}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"] = ([], set())
|
||||||
|
folder_paths.folder_names_and_paths["custom_nodes"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
assert str(tmp_path / "checkpoints") in folder_paths.folder_names_and_paths["checkpoints"][0]
|
||||||
|
assert str(tmp_path / "custom_nodes") not in folder_paths.folder_names_and_paths["custom_nodes"][0], \
|
||||||
|
"custom_nodes must not be auto-registered by the implicit scan"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_load_extra_path_config_explicit_overrides_implicit(
|
||||||
|
mock_yaml_load, clear_folder_paths, tmp_path
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Explicit sub-path declarations take precedence; the implicit scan must not
|
||||||
|
double-register a category that was already declared explicitly.
|
||||||
|
"""
|
||||||
|
(tmp_path / "loras").mkdir()
|
||||||
|
custom_loras = tmp_path / "my_custom_loras"
|
||||||
|
custom_loras.mkdir()
|
||||||
|
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"loras": "my_custom_loras", # explicit override
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
folder_paths.folder_names_and_paths["loras"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_model_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
registered = folder_paths.folder_names_and_paths["loras"][0]
|
||||||
|
assert str(custom_loras) in registered
|
||||||
|
assert str(tmp_path / "loras") not in registered, "Implicit path must not override explicit"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def save_restore_system_dirs():
|
||||||
|
"""Save and restore folder_paths system directories around a test."""
|
||||||
|
saved = {
|
||||||
|
"output": folder_paths.get_output_directory(),
|
||||||
|
"input": folder_paths.get_input_directory(),
|
||||||
|
"temp": folder_paths.get_temp_directory(),
|
||||||
|
"user": folder_paths.get_user_directory(),
|
||||||
|
}
|
||||||
|
yield
|
||||||
|
folder_paths.set_output_directory(saved["output"])
|
||||||
|
folder_paths.set_input_directory(saved["input"])
|
||||||
|
folder_paths.set_temp_directory(saved["temp"])
|
||||||
|
folder_paths.set_user_directory(saved["user"])
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_system_dir_keys(mock_yaml_load, save_restore_system_dirs, tmp_path):
|
||||||
|
"""System directory keys (output, input, temp, user) call set_*_directory()."""
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"output": "my_output/",
|
||||||
|
"input": "my_input/",
|
||||||
|
"temp": "my_temp/",
|
||||||
|
"user": "my_user/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path, allow_system_dirs=True)
|
||||||
|
|
||||||
|
assert folder_paths.get_output_directory() == os.path.normpath(str(tmp_path / "my_output"))
|
||||||
|
assert folder_paths.get_input_directory() == os.path.normpath(str(tmp_path / "my_input"))
|
||||||
|
assert folder_paths.get_temp_directory() == os.path.normpath(str(tmp_path / "my_temp"))
|
||||||
|
assert folder_paths.get_user_directory() == os.path.normpath(str(tmp_path / "my_user"))
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_system_dir_keys_not_applied_for_legacy(mock_yaml_load, save_restore_system_dirs, tmp_path):
|
||||||
|
"""System directory keys are ignored when allow_system_dirs=False (legacy extra_model_paths.yaml)."""
|
||||||
|
original_output = folder_paths.get_output_directory()
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"output": "my_output/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_model_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path) # allow_system_dirs defaults to False
|
||||||
|
|
||||||
|
# output should be unchanged — treated as a model category, not a system dir
|
||||||
|
assert folder_paths.get_output_directory() == original_output
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_nested_models_block(mock_yaml_load, clear_folder_paths, tmp_path):
|
||||||
|
"""Nested models: block registers model paths relative to models/base_path."""
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"models": {
|
||||||
|
"base_path": "models/",
|
||||||
|
"checkpoints": "checkpoints/",
|
||||||
|
"loras": "loras/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"] = ([], set())
|
||||||
|
folder_paths.folder_names_and_paths["loras"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
expected_ckpt = os.path.normpath(str(tmp_path / "models" / "checkpoints"))
|
||||||
|
expected_loras = os.path.normpath(str(tmp_path / "models" / "loras"))
|
||||||
|
assert expected_ckpt in folder_paths.folder_names_and_paths["checkpoints"][0]
|
||||||
|
assert expected_loras in folder_paths.folder_names_and_paths["loras"][0]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_nested_models_is_default(mock_yaml_load, clear_folder_paths, tmp_path):
|
||||||
|
"""is_default under models: applies to all model paths in that block."""
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"models": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"is_default": True,
|
||||||
|
"checkpoints": "checkpoints/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
mock_add = Mock()
|
||||||
|
with patch.object(folder_paths, "add_model_folder_path", mock_add):
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
call = mock_add.call_args_list[0]
|
||||||
|
assert call.args[0] == "checkpoints"
|
||||||
|
assert call.args[2] is True, "is_default under models: must be passed as True"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_nested_models_multipath(mock_yaml_load, clear_folder_paths, tmp_path):
|
||||||
|
"""Multi-line path values inside models: register multiple paths per category."""
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"models": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"text_encoders": "text_encoders/\nclip/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
folder_paths.folder_names_and_paths["text_encoders"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
registered = folder_paths.folder_names_and_paths["text_encoders"][0]
|
||||||
|
assert os.path.normpath(str(tmp_path / "text_encoders")) in registered
|
||||||
|
assert os.path.normpath(str(tmp_path / "clip")) in registered
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_nested_models_auto_scan(mock_yaml_load, clear_folder_paths, tmp_path):
|
||||||
|
"""models: with only base_path auto-scans for known categories that exist on disk."""
|
||||||
|
(tmp_path / "models" / "checkpoints").mkdir(parents=True)
|
||||||
|
(tmp_path / "models" / "loras").mkdir()
|
||||||
|
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"models": {"base_path": "models/"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"] = ([], set())
|
||||||
|
folder_paths.folder_names_and_paths["loras"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
assert os.path.normpath(str(tmp_path / "models" / "checkpoints")) in \
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"][0]
|
||||||
|
assert os.path.normpath(str(tmp_path / "models" / "loras")) in \
|
||||||
|
folder_paths.folder_names_and_paths["loras"][0]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_explicit_custom_nodes_key(mock_yaml_load, clear_folder_paths, tmp_path):
|
||||||
|
"""Explicit custom_nodes key in a block registers the path via add_model_folder_path."""
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"custom_nodes": "my_nodes/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
folder_paths.folder_names_and_paths["custom_nodes"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
assert os.path.normpath(str(tmp_path / "my_nodes")) in \
|
||||||
|
folder_paths.folder_names_and_paths["custom_nodes"][0]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("yaml.safe_load")
|
||||||
|
def test_nested_models_inherits_block_base(mock_yaml_load, clear_folder_paths, tmp_path):
|
||||||
|
"""models: block without its own base_path inherits the outer block's base_path."""
|
||||||
|
config_data = {
|
||||||
|
"comfyui": {
|
||||||
|
"base_path": str(tmp_path),
|
||||||
|
"is_default": True,
|
||||||
|
"models": {
|
||||||
|
"checkpoints": "models/checkpoints/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_yaml_load.return_value = config_data
|
||||||
|
folder_paths.folder_names_and_paths["checkpoints"] = ([], set())
|
||||||
|
|
||||||
|
yaml_path = str(tmp_path / "extra_paths.yaml")
|
||||||
|
with open(yaml_path, "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
load_extra_path_config(yaml_path)
|
||||||
|
|
||||||
|
expected = os.path.normpath(str(tmp_path / "models" / "checkpoints"))
|
||||||
|
paths = folder_paths.folder_names_and_paths["checkpoints"][0]
|
||||||
|
assert expected in paths
|
||||||
|
# is_default inherited: path should be at index 0
|
||||||
|
assert paths[0] == expected
|
||||||
|
|||||||
@ -1,34 +1,101 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
def load_extra_path_config(yaml_path):
|
_SYSTEM_DIR_KEYS = frozenset({"output", "input", "temp", "user"})
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_base(raw: str, parent_base: str | None, yaml_dir: str) -> str:
|
||||||
|
"""Resolve a base_path value: expand vars/user, join onto parent_base or yaml_dir if relative."""
|
||||||
|
raw = os.path.expandvars(os.path.expanduser(raw))
|
||||||
|
if not os.path.isabs(raw):
|
||||||
|
anchor = parent_base if parent_base else yaml_dir
|
||||||
|
raw = os.path.abspath(os.path.join(anchor, raw))
|
||||||
|
return os.path.normpath(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_model_paths(category: str, raw_value: str, base: str | None, yaml_dir: str, is_default: bool) -> None:
|
||||||
|
"""Split a (possibly multi-line) path value and register each path as a model folder."""
|
||||||
|
for raw in str(raw_value).split("\n"):
|
||||||
|
raw = raw.strip()
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
if base and not os.path.isabs(raw):
|
||||||
|
full_path = os.path.join(base, raw)
|
||||||
|
elif not os.path.isabs(raw):
|
||||||
|
full_path = os.path.abspath(os.path.join(yaml_dir, raw))
|
||||||
|
else:
|
||||||
|
full_path = raw
|
||||||
|
normalized = os.path.normpath(full_path)
|
||||||
|
logging.info("Adding extra search path %s %s", category, normalized)
|
||||||
|
folder_paths.add_model_folder_path(category, normalized, is_default)
|
||||||
|
|
||||||
|
|
||||||
|
def _implicit_scan(base: str, exclude: set[str], is_default: bool) -> None:
|
||||||
|
"""Auto-register base/<category>/ for known model categories that exist on disk.
|
||||||
|
|
||||||
|
custom_nodes and system directory keys are always excluded from the scan.
|
||||||
|
"""
|
||||||
|
skip = _SYSTEM_DIR_KEYS | {"custom_nodes"} | exclude
|
||||||
|
for category in folder_paths.folder_names_and_paths:
|
||||||
|
if category in skip:
|
||||||
|
continue
|
||||||
|
path = os.path.normpath(os.path.join(base, category))
|
||||||
|
if os.path.isdir(path):
|
||||||
|
logging.info("Adding extra search path %s %s", category, path)
|
||||||
|
folder_paths.add_model_folder_path(category, path, is_default)
|
||||||
|
|
||||||
|
|
||||||
|
def load_extra_path_config(yaml_path: str, allow_system_dirs: bool = False) -> None:
|
||||||
with open(yaml_path, 'r', encoding='utf-8') as stream:
|
with open(yaml_path, 'r', encoding='utf-8') as stream:
|
||||||
config = yaml.safe_load(stream)
|
config = yaml.safe_load(stream)
|
||||||
yaml_dir = os.path.dirname(os.path.abspath(yaml_path))
|
yaml_dir = os.path.dirname(os.path.abspath(yaml_path))
|
||||||
for c in config:
|
|
||||||
conf = config[c]
|
for _block_name, conf in config.items():
|
||||||
if conf is None:
|
if conf is None:
|
||||||
continue
|
continue
|
||||||
base_path = None
|
|
||||||
|
# Pop block-level meta keys (preserved for flat backward-compat style)
|
||||||
|
block_base = None
|
||||||
if "base_path" in conf:
|
if "base_path" in conf:
|
||||||
base_path = conf.pop("base_path")
|
block_base = _resolve_base(conf.pop("base_path"), None, yaml_dir)
|
||||||
base_path = os.path.expandvars(os.path.expanduser(base_path))
|
block_is_default = bool(conf.pop("is_default", False))
|
||||||
if not os.path.isabs(base_path):
|
|
||||||
base_path = os.path.abspath(os.path.join(yaml_dir, base_path))
|
has_models_block = False
|
||||||
is_default = False
|
flat_model_keys: set[str] = set()
|
||||||
if "is_default" in conf:
|
|
||||||
is_default = conf.pop("is_default")
|
for key, value in conf.items():
|
||||||
for x in conf:
|
if allow_system_dirs and key in _SYSTEM_DIR_KEYS:
|
||||||
for y in conf[x].split("\n"):
|
# System directory override → set_*_directory()
|
||||||
if len(y) == 0:
|
path = _resolve_base(str(value).strip(), block_base, yaml_dir)
|
||||||
continue
|
logging.info("Setting %s directory to %s", key, path)
|
||||||
full_path = y
|
getattr(folder_paths, f"set_{key}_directory")(path)
|
||||||
if base_path:
|
|
||||||
full_path = os.path.join(base_path, full_path)
|
elif key == "custom_nodes":
|
||||||
elif not os.path.isabs(full_path):
|
_add_model_paths("custom_nodes", value, block_base, yaml_dir, block_is_default)
|
||||||
full_path = os.path.abspath(os.path.join(yaml_dir, y))
|
|
||||||
normalized_path = os.path.normpath(full_path)
|
elif key == "models" and isinstance(value, dict):
|
||||||
logging.info("Adding extra search path {} {}".format(x, normalized_path))
|
# New nested style: models: { base_path, is_default, <categories> }
|
||||||
folder_paths.add_model_folder_path(x, normalized_path, is_default)
|
has_models_block = True
|
||||||
|
models_conf = dict(value)
|
||||||
|
models_base = block_base
|
||||||
|
if "base_path" in models_conf:
|
||||||
|
models_base = _resolve_base(models_conf.pop("base_path"), block_base, yaml_dir)
|
||||||
|
models_is_default = bool(models_conf.pop("is_default", block_is_default))
|
||||||
|
explicit: set[str] = set(models_conf.keys())
|
||||||
|
for cat, raw in models_conf.items():
|
||||||
|
_add_model_paths(cat, raw, models_base, yaml_dir, models_is_default)
|
||||||
|
if models_base:
|
||||||
|
_implicit_scan(models_base, explicit, models_is_default)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Flat model key — backward-compat style
|
||||||
|
_add_model_paths(key, value, block_base, yaml_dir, block_is_default)
|
||||||
|
flat_model_keys.add(key)
|
||||||
|
|
||||||
|
# Flat-style implicit scan (only when no nested models: block)
|
||||||
|
if block_base and not has_models_block:
|
||||||
|
_implicit_scan(block_base, flat_model_keys, block_is_default)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user