saliacoel commited on
Commit
db8f5c0
·
verified ·
1 Parent(s): 2532fd1

Upload 2 files

Browse files
Files changed (2) hide show
  1. salia_console_get_node.py +175 -0
  2. salia_get_lora_x.py +157 -0
salia_console_get_node.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shlex
3
+ import subprocess
4
+ from urllib.parse import urlparse
5
+
6
+
7
+ NODES_PATH = "/ComfyUI/custom_nodes/comfyui-salia_online/nodes"
8
+ DEFAULT_REPO_PATH = "https://huggingface.co/saliacoel/x/resolve/main/"
9
+
10
+
11
+ class Salia_Console_Get_Node:
12
+ @classmethod
13
+ def INPUT_TYPES(cls):
14
+ return {
15
+ "required": {
16
+ "items": (
17
+ "STRING",
18
+ {
19
+ "default": "",
20
+ "multiline": True,
21
+ "placeholder": "salia_example_node_1, salia_example_node_2.py, https://huggingface.co/other_hf/other_repo/resolve/main/salia_example_node_3.py, salia_example_4_nodepack.zip",
22
+ },
23
+ ),
24
+ },
25
+ }
26
+
27
+ RETURN_TYPES = ("STRING",)
28
+ RETURN_NAMES = ("status",)
29
+ FUNCTION = "get_nodes"
30
+ CATEGORY = "salia/online"
31
+ OUTPUT_NODE = True
32
+
33
+ @classmethod
34
+ def IS_CHANGED(cls, items):
35
+ return float("nan")
36
+
37
+ def _log(self, message: str):
38
+ print(f"[Salia_Console_Get_Node] {message}")
39
+
40
+ def _split_items(self, items: str):
41
+ parts = items.replace("\n", ",").split(",")
42
+ cleaned = []
43
+
44
+ for part in parts:
45
+ part = part.strip().strip('"').strip("'")
46
+ if part:
47
+ cleaned.append(part)
48
+
49
+ return cleaned
50
+
51
+ def _is_url(self, value: str) -> bool:
52
+ # Accept both https:// and http:// as already-complete URLs.
53
+ return value.startswith(("https://", "http://"))
54
+
55
+ def _has_supported_extension(self, value: str) -> bool:
56
+ target = urlparse(value).path if self._is_url(value) else value
57
+ target = target.lower()
58
+ return target.endswith(".py") or target.endswith(".zip")
59
+
60
+ def _normalize_item(self, item: str) -> str:
61
+ item = item.strip().strip('"').strip("'")
62
+
63
+ # 1) Add .py if it is neither .py nor .zip
64
+ if not self._has_supported_extension(item):
65
+ item += ".py"
66
+
67
+ # 2) Prefix default Hugging Face repo if it is not already a URL
68
+ if not self._is_url(item):
69
+ item = DEFAULT_REPO_PATH + item.lstrip("/")
70
+
71
+ return item
72
+
73
+ def _filename_from_url(self, url: str) -> str:
74
+ filename = os.path.basename(urlparse(url).path)
75
+ if not filename:
76
+ raise ValueError(f"Could not determine filename from URL: {url}")
77
+ return filename
78
+
79
+ def _run_command(self, args, cwd=None):
80
+ command_text = " ".join(shlex.quote(str(arg)) for arg in args)
81
+ if cwd:
82
+ self._log(f"$ cd {cwd} && {command_text}")
83
+ else:
84
+ self._log(f"$ {command_text}")
85
+
86
+ completed = subprocess.run(
87
+ args,
88
+ cwd=cwd,
89
+ capture_output=True,
90
+ text=True,
91
+ )
92
+
93
+ if completed.stdout:
94
+ print(completed.stdout)
95
+ if completed.stderr:
96
+ print(completed.stderr)
97
+
98
+ return completed
99
+
100
+ def get_nodes(self, items: str):
101
+ entries = self._split_items(items)
102
+
103
+ if not entries:
104
+ self._log("No items provided.")
105
+ return ("OK: 0\nERROR: 0",)
106
+
107
+ total_count = len(entries)
108
+ ok_count = total_count
109
+ error_count = 0
110
+
111
+ try:
112
+ os.makedirs(NODES_PATH, exist_ok=True)
113
+ except Exception as e:
114
+ self._log(f"Could not create/access nodes path: {e}")
115
+ return (f"OK: 0\nERROR: {total_count}",)
116
+
117
+ for original_item in entries:
118
+ try:
119
+ url = self._normalize_item(original_item)
120
+ filename = self._filename_from_url(url)
121
+ lower_filename = filename.lower()
122
+
123
+ self._log(f"Resolved: {original_item} -> {url}")
124
+
125
+ if lower_filename.endswith(".py"):
126
+ completed = self._run_command(
127
+ ["wget", "-O", filename, url],
128
+ cwd=NODES_PATH,
129
+ )
130
+ if completed.returncode != 0:
131
+ raise RuntimeError(f"wget failed for {filename}")
132
+
133
+ elif lower_filename.endswith(".zip"):
134
+ completed = self._run_command(["apt-get", "update"])
135
+ if completed.returncode != 0:
136
+ raise RuntimeError("apt-get update failed")
137
+
138
+ completed = self._run_command(["apt-get", "install", "-y", "unzip"])
139
+ if completed.returncode != 0:
140
+ raise RuntimeError("apt-get install -y unzip failed")
141
+
142
+ completed = self._run_command(
143
+ ["wget", "-O", filename, url],
144
+ cwd=NODES_PATH,
145
+ )
146
+ if completed.returncode != 0:
147
+ raise RuntimeError(f"wget failed for {filename}")
148
+
149
+ completed = self._run_command(
150
+ ["unzip", "-o", filename],
151
+ cwd=NODES_PATH,
152
+ )
153
+ if completed.returncode != 0:
154
+ raise RuntimeError(f"unzip failed for {filename}")
155
+
156
+ else:
157
+ raise RuntimeError(f"Unsupported file type: {filename}")
158
+
159
+ except Exception as e:
160
+ error_count += 1
161
+ ok_count -= 1
162
+ self._log(f"ERROR for '{original_item}': {e}")
163
+
164
+ summary = f"OK: {ok_count}\nERROR: {error_count}"
165
+ self._log(summary.replace("\n", " | "))
166
+ return (summary,)
167
+
168
+
169
+ NODE_CLASS_MAPPINGS = {
170
+ "Salia_Console_Get_Node": Salia_Console_Get_Node,
171
+ }
172
+
173
+ NODE_DISPLAY_NAME_MAPPINGS = {
174
+ "Salia_Console_Get_Node": "Salia_Console_Get_Node",
175
+ }
salia_get_lora_x.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import shutil
4
+ import urllib.parse
5
+ import urllib.request
6
+
7
+ import comfy.sd
8
+ import comfy.utils
9
+ import folder_paths
10
+
11
+
12
+ HF_REPO_BASE = "https://huggingface.co/saliacoel/x/resolve/main"
13
+
14
+
15
+ class Salia_Get_Lora_X:
16
+ """
17
+ Loads a single LoRA from the public Hugging Face repo `saliacoel/x`.
18
+
19
+ Behavior:
20
+ - Takes a single base name, e.g. `Fade_to_Black`
21
+ - Normalizes it to `Fade_to_Black.safetensors`
22
+ - Checks if it already exists in ComfyUI's loras folders
23
+ - If missing, downloads it
24
+ - Loads it and applies it model-only to one MODEL input
25
+ """
26
+
27
+ CATEGORY = "loaders/saliacoel"
28
+ FUNCTION = "load_lora"
29
+ RETURN_TYPES = ("MODEL",)
30
+ RETURN_NAMES = ("loaded_model",)
31
+ DESCRIPTION = (
32
+ "Loads a single LoRA from saliacoel/x. If missing locally, it is "
33
+ "downloaded first, then applied model-only."
34
+ )
35
+ OUTPUT_TOOLTIPS = (
36
+ "model_in with the LoRA applied.",
37
+ )
38
+
39
+ def __init__(self):
40
+ self.loaded_lora = None
41
+
42
+ @classmethod
43
+ def INPUT_TYPES(cls):
44
+ return {
45
+ "required": {
46
+ "filename": (
47
+ "STRING",
48
+ {
49
+ "default": "",
50
+ "multiline": False,
51
+ "placeholder": "Fade_to_Black",
52
+ "tooltip": "LoRA name without .safetensors",
53
+ },
54
+ ),
55
+ "model_in": (
56
+ "MODEL",
57
+ {"tooltip": "The MODEL input that will receive the LoRA."},
58
+ ),
59
+ "strength": (
60
+ "FLOAT",
61
+ {
62
+ "default": 1.0,
63
+ "min": -100.0,
64
+ "max": 100.0,
65
+ "step": 0.01,
66
+ "tooltip": "Strength used when applying the LoRA.",
67
+ },
68
+ ),
69
+ }
70
+ }
71
+
72
+ @staticmethod
73
+ def _normalize_lora_name(name: str) -> str:
74
+ base = os.path.basename((name or "").strip())
75
+ if not base:
76
+ raise ValueError("filename cannot be empty.")
77
+
78
+ if base.lower().endswith(".safetensors"):
79
+ base = base[: -len(".safetensors")]
80
+
81
+ if not base:
82
+ raise ValueError("filename resolves to an empty name.")
83
+
84
+ return f"{base}.safetensors"
85
+
86
+ @staticmethod
87
+ def _download_file(url: str, target_path: str) -> None:
88
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
89
+ tmp_path = target_path + ".download"
90
+
91
+ if os.path.exists(tmp_path):
92
+ os.remove(tmp_path)
93
+
94
+ try:
95
+ request = urllib.request.Request(
96
+ url,
97
+ headers={"User-Agent": "ComfyUI-SaliacoelSingleRepoLora/1.0"},
98
+ )
99
+ with urllib.request.urlopen(request) as response, open(tmp_path, "wb") as out_file:
100
+ shutil.copyfileobj(response, out_file)
101
+
102
+ os.replace(tmp_path, target_path)
103
+ except Exception:
104
+ if os.path.exists(tmp_path):
105
+ os.remove(tmp_path)
106
+ raise
107
+
108
+ @classmethod
109
+ def _ensure_lora_available(cls, lora_name: str) -> str:
110
+ existing_path = folder_paths.get_full_path("loras", lora_name)
111
+ if existing_path is not None:
112
+ return existing_path
113
+
114
+ lora_dirs = folder_paths.get_folder_paths("loras")
115
+ if not lora_dirs:
116
+ raise RuntimeError("No ComfyUI 'loras' folder is configured.")
117
+
118
+ target_dir = lora_dirs[0]
119
+ target_path = os.path.join(target_dir, lora_name)
120
+ url = f"{HF_REPO_BASE}/{urllib.parse.quote(lora_name)}"
121
+
122
+ logging.info("[SaliacoelSingleRepoLora] Downloading missing LoRA: %s", url)
123
+ cls._download_file(url, target_path)
124
+
125
+ resolved_path = folder_paths.get_full_path("loras", lora_name)
126
+ return resolved_path if resolved_path is not None else target_path
127
+
128
+ def _get_or_load_lora(self, lora_path: str):
129
+ if self.loaded_lora is not None and self.loaded_lora[0] == lora_path:
130
+ return self.loaded_lora[1]
131
+
132
+ lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
133
+ self.loaded_lora = (lora_path, lora)
134
+ return lora
135
+
136
+ def _apply_lora_model_only(self, model, lora_path: str, strength: float):
137
+ if strength == 0:
138
+ return model
139
+
140
+ lora = self._get_or_load_lora(lora_path)
141
+ model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength, 0)
142
+ return model_lora
143
+
144
+ def load_lora(self, filename, model_in, strength):
145
+ lora_name = self._normalize_lora_name(filename)
146
+ lora_path = self._ensure_lora_available(lora_name)
147
+ loaded_model = self._apply_lora_model_only(model_in, lora_path, strength)
148
+ return (loaded_model,)
149
+
150
+
151
+ NODE_CLASS_MAPPINGS = {
152
+ "Salia_Get_Lora_X": Salia_Get_Lora_X,
153
+ }
154
+
155
+ NODE_DISPLAY_NAME_MAPPINGS = {
156
+ "Salia_Get_Lora_X": "Salia Get LoRa and Load LoRa X (1)",
157
+ }