change single selector

This commit is contained in:
Geundol222
2025-12-14 22:31:08 +09:00
parent e43e8356af
commit 39ee5100be
5 changed files with 110 additions and 99 deletions
+19 -25
View File
@@ -1,27 +1,21 @@
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io, _io
from comfy_api.latest import ComfyExtension, io
import folder_paths
from .tree_utils import attach_tree_metadata, build_tree, list_dirs, list_files, resolve_selected_path, sanitize_rel_dir
from .tree_utils import attach_tree_metadata, build_tree, list_files, resolve_selected_path
def build_folder_file_input(folder_id: str, file_id: str, folder_type: str, tooltip: str | None = None) -> _io.DynamicCombo.Input:
options: list[_io.DynamicCombo.Option] = []
for rel_dir in list_dirs(folder_type):
file_options = list_files(folder_type, rel_dir)
if not file_options:
continue
child_id = f"{file_id}__{sanitize_rel_dir(rel_dir)}"
inputs = [io.Combo.Input(child_id, options=file_options, tooltip=tooltip)]
options.append(_io.DynamicCombo.Option(rel_dir if rel_dir else "root", inputs))
def build_file_input(input_id: str, folder_type: str, tooltip: str | None = None) -> io.Combo.Input:
options: list[str] = []
for rel_path in list_files(folder_type, ""):
options.append(rel_path)
if not options:
child_id = f"{file_id}__none"
options.append(_io.DynamicCombo.Option("No files found", [io.Combo.Input(child_id, options=["<none>"], tooltip="No files found")]))
combo = _io.DynamicCombo.Input(folder_id, options=options, tooltip="Select folder")
tree = build_tree(folder_type, file_id)
return attach_tree_metadata(combo, tree, tooltip="Select folder")
options = ["<none>"]
combo = io.Combo.Input(input_id, options=sorted(set(options)), tooltip=tooltip or "Select file")
tree = build_tree(folder_type, input_id)
return attach_tree_metadata(combo, tree, tooltip=tooltip or "Select file")
class CheckpointLoader(io.ComfyNode):
@@ -35,7 +29,7 @@ class CheckpointLoader(io.ComfyNode):
category="SK Loader/Advanced",
inputs=[
io.Combo.Input("config_name", options=folder_paths.get_filename_list("configs")),
build_folder_file_input("ckpt_folder", "ckpt_name", "checkpoints"),
build_file_input("ckpt", "checkpoints"),
],
outputs=[
io.Model.Output(),
@@ -45,11 +39,11 @@ class CheckpointLoader(io.ComfyNode):
)
@classmethod
def execute(cls, config_name: str, ckpt_folder: dict) -> io.NodeOutput:
def execute(cls, config_name: str, ckpt: dict | str) -> io.NodeOutput:
import comfy.sd
config_path = folder_paths.get_full_path("configs", config_name)
ckpt_path = resolve_selected_path("checkpoints", ckpt_folder, "ckpt_folder", "ckpt_name")
ckpt_path = resolve_selected_path("checkpoints", ckpt, "ckpt", "ckpt")
loaded = comfy.sd.load_checkpoint(
config_path,
ckpt_path,
@@ -70,7 +64,7 @@ class CheckpointLoaderSimple(io.ComfyNode):
display_name="[SK] Checkpoint",
category="SK Loader",
inputs=[
build_folder_file_input("ckpt_folder", "ckpt_name", "checkpoints", tooltip="The checkpoint (model) to load."),
build_file_input("ckpt", "checkpoints", tooltip="The checkpoint (model) to load."),
],
outputs=[
io.Model.Output(),
@@ -81,10 +75,10 @@ class CheckpointLoaderSimple(io.ComfyNode):
)
@classmethod
def execute(cls, ckpt_folder: dict) -> io.NodeOutput:
def execute(cls, ckpt: dict | str) -> io.NodeOutput:
import comfy.sd
ckpt_path = resolve_selected_path("checkpoints", ckpt_folder, "ckpt_folder", "ckpt_name")
ckpt_path = resolve_selected_path("checkpoints", ckpt, "ckpt", "ckpt")
out = comfy.sd.load_checkpoint_guess_config(
ckpt_path,
output_vae=True,
@@ -104,7 +98,7 @@ class unCLIPCheckpointLoader(io.ComfyNode):
display_name="[SK] unCLIP Checkpoint",
category="SK Loader",
inputs=[
build_folder_file_input("ckpt_folder", "ckpt_name", "checkpoints"),
build_file_input("ckpt", "checkpoints"),
],
outputs=[
io.Model.Output(),
@@ -115,10 +109,10 @@ class unCLIPCheckpointLoader(io.ComfyNode):
)
@classmethod
def execute(cls, ckpt_folder: dict) -> io.NodeOutput:
def execute(cls, ckpt: dict | str) -> io.NodeOutput:
import comfy.sd
ckpt_path = resolve_selected_path("checkpoints", ckpt_folder, "ckpt_folder", "ckpt_name")
ckpt_path = resolve_selected_path("checkpoints", ckpt, "ckpt", "ckpt")
out = comfy.sd.load_checkpoint_guess_config(
ckpt_path,
output_vae=True,
+13 -19
View File
@@ -1,27 +1,21 @@
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io, _io
from comfy_api.latest import ComfyExtension, io
import folder_paths
from .tree_utils import attach_tree_metadata, build_tree, list_dirs, list_files, resolve_selected_path, sanitize_rel_dir
from .tree_utils import attach_tree_metadata, build_tree, list_files, resolve_selected_path
def build_folder_file_input(folder_id: str, file_id: str, folder_type: str, tooltip: str | None = None) -> _io.DynamicCombo.Input:
options: list[_io.DynamicCombo.Option] = []
for rel_dir in list_dirs(folder_type):
file_options = list_files(folder_type, rel_dir)
if not file_options:
continue
child_id = f"{file_id}__{sanitize_rel_dir(rel_dir)}"
inputs = [io.Combo.Input(child_id, options=file_options, tooltip=tooltip)]
options.append(_io.DynamicCombo.Option(rel_dir if rel_dir else "root", inputs))
def build_file_input(input_id: str, folder_type: str, tooltip: str | None = None) -> io.Combo.Input:
options: list[str] = []
for rel_path in list_files(folder_type, ""):
options.append(rel_path)
if not options:
child_id = f"{file_id}__none"
options.append(_io.DynamicCombo.Option("No files found", [io.Combo.Input(child_id, options=["<none>"], tooltip="No files found")]))
combo = _io.DynamicCombo.Input(folder_id, options=options, tooltip="Select folder")
tree = build_tree(folder_type, file_id)
return attach_tree_metadata(combo, tree, tooltip="Select folder")
options = ["<none>"]
combo = io.Combo.Input(input_id, options=sorted(set(options)), tooltip=tooltip or "Select file")
tree = build_tree(folder_type, input_id)
return attach_tree_metadata(combo, tree, tooltip=tooltip or "Select file")
class UNETLoader(io.ComfyNode):
@@ -34,7 +28,7 @@ class UNETLoader(io.ComfyNode):
display_name="[SK] Diffusion Model",
category="SK Loader/Advanced",
inputs=[
build_folder_file_input("unet_folder", "unet_name", "diffusion_models"),
build_file_input("unet", "diffusion_models"),
io.Combo.Input(
"weight_dtype",
options=["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],
@@ -46,7 +40,7 @@ class UNETLoader(io.ComfyNode):
)
@classmethod
def execute(cls, unet_folder: dict, weight_dtype: str) -> io.NodeOutput:
def execute(cls, unet: dict | str, weight_dtype: str) -> io.NodeOutput:
import torch
import comfy.sd
@@ -59,7 +53,7 @@ class UNETLoader(io.ComfyNode):
elif weight_dtype == "fp8_e5m2":
model_options["dtype"] = torch.float8_e5m2
unet_path = resolve_selected_path("diffusion_models", unet_folder, "unet_folder", "unet_name")
unet_path = resolve_selected_path("diffusion_models", unet, "unet", "unet")
model = comfy.sd.load_diffusion_model(unet_path, model_options=model_options)
return io.NodeOutput(model)
+23 -28
View File
@@ -1,25 +1,20 @@
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io, _io
from comfy_api.latest import ComfyExtension, io
from .tree_utils import attach_tree_metadata, build_tree, list_dirs, list_files, resolve_selected_path, sanitize_rel_dir
from .tree_utils import attach_tree_metadata, build_tree, list_files, resolve_selected_path
def build_folder_file_input(folder_id: str, file_id: str, folder_type: str, tooltip: str | None = None) -> _io.DynamicCombo.Input:
options: list[_io.DynamicCombo.Option] = []
for rel_dir in list_dirs(folder_type):
file_options = list_files(folder_type, rel_dir)
if not file_options:
continue
child_id = f"{file_id}__{sanitize_rel_dir(rel_dir)}"
inputs = [io.Combo.Input(child_id, options=file_options, tooltip=tooltip)]
options.append(_io.DynamicCombo.Option(rel_dir if rel_dir else "root", inputs))
def build_file_input(input_id: str, folder_type: str, tooltip: str | None = None) -> io.Combo.Input:
"""Single combo with tree metadata over all files."""
options: list[str] = []
for rel_path in list_files(folder_type, ""):
options.append(rel_path)
if not options:
child_id = f"{file_id}__none"
options.append(_io.DynamicCombo.Option("No files found", [io.Combo.Input(child_id, options=["<none>"], tooltip="No files found")]))
combo = _io.DynamicCombo.Input(folder_id, options=options, tooltip="Select folder")
tree = build_tree(folder_type, file_id)
return attach_tree_metadata(combo, tree, tooltip="Select folder")
options = ["<none>"]
combo = io.Combo.Input(input_id, options=sorted(set(options)), tooltip=tooltip or "Select file")
tree = build_tree(folder_type, input_id)
return attach_tree_metadata(combo, tree, tooltip=tooltip or "Select file")
def build_lora_slot_inputs(idx: int) -> list:
@@ -28,7 +23,7 @@ def build_lora_slot_inputs(idx: int) -> list:
label = f"LoRA #{idx}"
return [
io.Boolean.Input(f"{prefix}_enabled", default=False, tooltip=f"Enable {label}"),
build_folder_file_input(f"{prefix}_folder", f"{prefix}_name", "loras", tooltip=f"Select {label}"),
build_file_input(prefix, "loras", tooltip=f"Select {label}"),
io.Float.Input(
f"{prefix}_strength_model",
default=1.0,
@@ -60,7 +55,7 @@ class LoraLoader(io.ComfyNode):
inputs=[
io.Model.Input("model", tooltip="The diffusion model the LoRA will be applied to."),
io.Clip.Input("clip", tooltip="The CLIP model the LoRA will be applied to."),
build_folder_file_input("lora_folder", "lora_name", "loras", tooltip="The name of the LoRA."),
build_file_input("lora", "loras", tooltip="The name of the LoRA."),
io.Float.Input(
"strength_model",
default=1.0,
@@ -86,20 +81,20 @@ class LoraLoader(io.ComfyNode):
)
@classmethod
def _apply_lora(cls, model, clip, selection: dict, strength_model: float, strength_clip: float):
def _apply_lora(cls, model, clip, selection: dict | str, strength_model: float, strength_clip: float):
import comfy.sd
import comfy.utils
if strength_model == 0 and strength_clip == 0:
return model, clip
lora_path = resolve_selected_path("loras", selection, "lora_folder", "lora_name")
lora_path = resolve_selected_path("loras", selection, "lora", "lora")
loaded = comfy.utils.load_torch_file(lora_path, safe_load=True)
return comfy.sd.load_lora_for_models(model, clip, loaded, strength_model, strength_clip)
@classmethod
def execute(cls, model, clip, lora_folder: dict, strength_model: float, strength_clip: float) -> io.NodeOutput:
model_lora, clip_lora = cls._apply_lora(model, clip, lora_folder, strength_model, strength_clip)
def execute(cls, model, clip, lora: dict | str, strength_model: float, strength_clip: float) -> io.NodeOutput:
model_lora, clip_lora = cls._apply_lora(model, clip, lora, strength_model, strength_clip)
return io.NodeOutput(model_lora, clip_lora)
@@ -114,7 +109,7 @@ class LoraLoaderModelOnly(LoraLoader):
category="SK Loader",
inputs=[
io.Model.Input("model"),
build_folder_file_input("lora_folder", "lora_name", "loras"),
build_file_input("lora", "loras"),
io.Float.Input(
"strength_model",
default=1.0,
@@ -129,8 +124,8 @@ class LoraLoaderModelOnly(LoraLoader):
)
@classmethod
def execute(cls, model, lora_folder: dict, strength_model: float) -> io.NodeOutput:
model_lora, _ = cls._apply_lora(model, None, lora_folder, strength_model, 0)
def execute(cls, model, lora: dict | str, strength_model: float) -> io.NodeOutput:
model_lora, _ = cls._apply_lora(model, None, lora, strength_model, 0)
return io.NodeOutput(model_lora)
@@ -170,8 +165,8 @@ class PowerLoraLoader(io.ComfyNode):
if not enabled:
continue
selection = kwargs.get(f"lora_{idx}_folder")
if not isinstance(selection, dict):
selection = kwargs.get(f"lora_{idx}")
if not isinstance(selection, (dict, str)):
continue
strength_model = float(kwargs.get(f"lora_{idx}_strength_model", 1.0))
@@ -180,7 +175,7 @@ class PowerLoraLoader(io.ComfyNode):
continue
try:
lora_path = resolve_selected_path("loras", selection, f"lora_{idx}_folder", f"lora_{idx}_name")
lora_path = resolve_selected_path("loras", selection, f"lora_{idx}", f"lora_{idx}")
except FileNotFoundError:
continue
+21 -2
View File
@@ -105,11 +105,30 @@ def build_tree(folder_type: str, file_id: str, extra_roots: list[dict[str, Any]]
return tree
def resolve_selected_path(folder_type: str, selection: dict, folder_id: str, file_id: str) -> str:
"""Resolve a selection map (combo or tree UI) to an absolute path under the given folder type."""
def resolve_selected_path(folder_type: str, selection: dict | str, folder_id: str | None = None, file_id: str | None = None) -> str:
"""Resolve a selection (string or legacy dict) to an absolute path under the given folder type."""
# Simple string selection: try absolute, then relative to the folder_type roots.
if isinstance(selection, str):
rel = selection.strip()
if rel in ("", "<none>"):
raise FileNotFoundError("No file selected")
rel = rel.replace("\\", "/")
if os.path.isabs(rel) and os.path.exists(rel):
return rel
rel_no_prefix = rel[len(folder_type) + 1 :] if rel.startswith(f"{folder_type}/") else rel
for candidate_rel in (rel_no_prefix, rel):
for base in folder_paths.get_folder_paths(folder_type):
candidate = os.path.join(base, candidate_rel)
if os.path.exists(candidate):
return candidate
return rel # last resort
if not isinstance(selection, dict):
raise ValueError("Invalid selection")
folder_id = folder_id or ""
file_id = file_id or ""
def _valid(val: Any) -> bool:
return isinstance(val, str) and val not in ("", "<none>")
+34 -25
View File
@@ -3,7 +3,7 @@ from typing import Any
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io, _io
from comfy_api.latest import ComfyExtension, io
import folder_paths
from .tree_utils import ALLOWED_EXT, attach_tree_metadata, list_dirs, list_files, sanitize_rel_dir, _ensure_branch
@@ -66,34 +66,43 @@ def build_vae_tree(file_id: str) -> list[dict[str, Any]]:
return tree
def build_vae_input(folder_id: str, file_id: str) -> _io.DynamicCombo.Input:
options: list[_io.DynamicCombo.Option] = []
def build_vae_input(input_id: str) -> io.Combo.Input:
"""Single select with tree metadata; options = builtins + all vae/vae_approx files."""
options: list[str] = []
# built-in options
options.append(
_io.DynamicCombo.Option(
"builtins",
[io.Combo.Input(f"{file_id}__builtins", options=BUILTIN_VAES, tooltip="Built-in VAE/TAES")],
)
)
options.extend(BUILTIN_VAES)
for folder_type in ("vae", "vae_approx"):
for rel_dir in list_dirs(folder_type):
file_options = list_files(folder_type, rel_dir)
if not file_options:
continue
label = f"{folder_type}/{rel_dir}" if rel_dir else folder_type
child_id = f"{file_id}__{sanitize_rel_dir(label)}"
inputs = [io.Combo.Input(child_id, options=file_options, tooltip="Select VAE file")]
options.append(_io.DynamicCombo.Option(label, inputs))
for rel_path in list_files(folder_type, ""):
options.append(f"{folder_type}/{rel_path}")
if not options:
options.append(_io.DynamicCombo.Option("No files found", [io.Combo.Input(f"{file_id}__none", options=["<none>"], tooltip="No files found")]))
combo = _io.DynamicCombo.Input(folder_id, options=options, tooltip="Select folder")
tree = build_vae_tree(file_id)
return attach_tree_metadata(combo, tree, tooltip="Select folder")
options = ["<none>"]
combo = io.Combo.Input(input_id, options=sorted(set(options)), tooltip="Select VAE")
tree = build_vae_tree(input_id)
return attach_tree_metadata(combo, tree, tooltip="Select VAE")
def resolve_selected_path(selection: dict, folder_id: str, file_id: str) -> str:
def resolve_selected_path(selection: dict | str, folder_id: str = "vae_folder", file_id: str = "vae_name") -> str:
"""Resolve tree-aware single select or legacy two-combo selection to a usable path or builtin."""
if isinstance(selection, str):
rel = selection.strip()
if rel in BUILTIN_VAES:
return rel
rel = rel.replace("\\", "/")
if os.path.isabs(rel) and os.path.exists(rel):
return rel
for folder_type in ("vae", "vae_approx"):
rel_no_prefix = rel[len(folder_type) + 1 :] if rel.startswith(f"{folder_type}/") else rel
for candidate_rel in (rel_no_prefix, rel):
for base in folder_paths.get_folder_paths(folder_type):
candidate = os.path.join(base, candidate_rel)
if os.path.exists(candidate):
return candidate
return rel # last resort
if not isinstance(selection, dict):
raise ValueError("Invalid selection")
folder_sel = selection.get(folder_id, "builtins")
@@ -182,7 +191,7 @@ class VAELoader(io.ComfyNode):
display_name="[SK] VAE",
category="SK Loader",
inputs=[
build_vae_input("vae_folder", "vae_name"),
build_vae_input("vae"),
],
outputs=[
io.Vae.Output(),
@@ -191,12 +200,12 @@ class VAELoader(io.ComfyNode):
# TODO: scale factor?
@classmethod
def execute(cls, vae_folder: dict) -> io.NodeOutput:
def execute(cls, vae: dict | str) -> io.NodeOutput:
import torch
import comfy.sd
import comfy.utils
resolved = resolve_selected_path(vae_folder, "vae_folder", "vae_name")
resolved = resolve_selected_path(vae, "vae_folder", "vae_name")
# Check if it's a builtin VAE
if resolved in ("pixel_space", "taesd", "taesdxl", "taesd3", "taef1"):