comfy-webui-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_webui_core/__init__.py +73 -0
- comfy_webui_core/backend_manager.py +70 -0
- comfy_webui_core/chain_injectors/__init__.py +0 -0
- comfy_webui_core/chain_injectors/easycache_injector.py +40 -0
- comfy_webui_core/chain_injectors/lora_injector.py +67 -0
- comfy_webui_core/chain_injectors/lora_model_only_injector.py +59 -0
- comfy_webui_core/chain_injectors/reference_latent_injector.py +157 -0
- comfy_webui_core/comfy_api.py +482 -0
- comfy_webui_core/config.py +89 -0
- comfy_webui_core/context.py +33 -0
- comfy_webui_core/download_utils.py +147 -0
- comfy_webui_core/input_processors.py +53 -0
- comfy_webui_core/job_manager.py +129 -0
- comfy_webui_core/model_downloader.py +170 -0
- comfy_webui_core/node_info_manager.py +114 -0
- comfy_webui_core/shared_ui.py +327 -0
- comfy_webui_core/ui_builder.py +277 -0
- comfy_webui_core/ui_loader.py +83 -0
- comfy_webui_core/utils.py +199 -0
- comfy_webui_core/workflow_assembler.py +235 -0
- comfy_webui_core/yaml_loader.py +80 -0
- comfy_webui_core-0.1.0.dist-info/METADATA +84 -0
- comfy_webui_core-0.1.0.dist-info/RECORD +27 -0
- comfy_webui_core-0.1.0.dist-info/WHEEL +5 -0
- comfy_webui_core-0.1.0.dist-info/licenses/LICENSE +201 -0
- comfy_webui_core-0.1.0.dist-info/licenses/NOTICE +5 -0
- comfy_webui_core-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comfy Web UI Core Framework.
|
|
3
|
+
|
|
4
|
+
Provides dynamic ComfyUI workflow engine, Gradio WebUI dynamic assembly,
|
|
5
|
+
MCP server endpoints, multi-backend routing, and chain injector topology.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .context import set_app_root, get_app_root
|
|
9
|
+
from .workflow_assembler import WorkflowAssembler
|
|
10
|
+
from .ui_builder import build_gradio_ui
|
|
11
|
+
from .ui_loader import discover_ui_modules, load_ui_layout
|
|
12
|
+
from .backend_manager import backend_manager
|
|
13
|
+
from .node_info_manager import (
|
|
14
|
+
fetch_and_cache_object_info,
|
|
15
|
+
get_node_info,
|
|
16
|
+
get_all_node_info,
|
|
17
|
+
)
|
|
18
|
+
from .job_manager import (
|
|
19
|
+
create_job,
|
|
20
|
+
get_job,
|
|
21
|
+
update_job,
|
|
22
|
+
get_latest_running_job_for_module,
|
|
23
|
+
)
|
|
24
|
+
from .yaml_loader import load_and_merge_yaml, load_and_merge_yaml_from_module
|
|
25
|
+
|
|
26
|
+
from . import config
|
|
27
|
+
from . import workflow_assembler
|
|
28
|
+
from . import ui_builder
|
|
29
|
+
from . import ui_loader
|
|
30
|
+
from . import backend_manager as backend_manager_mod
|
|
31
|
+
from . import node_info_manager as node_info_manager_mod
|
|
32
|
+
from . import job_manager as job_manager_mod
|
|
33
|
+
from . import comfy_api
|
|
34
|
+
from . import shared_ui
|
|
35
|
+
from . import utils
|
|
36
|
+
from . import input_processors
|
|
37
|
+
from . import yaml_loader
|
|
38
|
+
from . import download_utils
|
|
39
|
+
from . import model_downloader
|
|
40
|
+
from . import chain_injectors
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"set_app_root",
|
|
46
|
+
"get_app_root",
|
|
47
|
+
"WorkflowAssembler",
|
|
48
|
+
"build_gradio_ui",
|
|
49
|
+
"discover_ui_modules",
|
|
50
|
+
"load_ui_layout",
|
|
51
|
+
"backend_manager",
|
|
52
|
+
"fetch_and_cache_object_info",
|
|
53
|
+
"get_node_info",
|
|
54
|
+
"get_all_node_info",
|
|
55
|
+
"create_job",
|
|
56
|
+
"get_job",
|
|
57
|
+
"update_job",
|
|
58
|
+
"get_latest_running_job_for_module",
|
|
59
|
+
"load_and_merge_yaml",
|
|
60
|
+
"load_and_merge_yaml_from_module",
|
|
61
|
+
"config",
|
|
62
|
+
"workflow_assembler",
|
|
63
|
+
"ui_builder",
|
|
64
|
+
"ui_loader",
|
|
65
|
+
"comfy_api",
|
|
66
|
+
"shared_ui",
|
|
67
|
+
"utils",
|
|
68
|
+
"input_processors",
|
|
69
|
+
"yaml_loader",
|
|
70
|
+
"download_utils",
|
|
71
|
+
"model_downloader",
|
|
72
|
+
"chain_injectors",
|
|
73
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import threading
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from .config import COMFYUI_BACKENDS
|
|
5
|
+
|
|
6
|
+
class BackendManager:
|
|
7
|
+
_instance = None
|
|
8
|
+
_lock = threading.Lock()
|
|
9
|
+
|
|
10
|
+
def __new__(cls, *args, **kwargs):
|
|
11
|
+
if not cls._instance:
|
|
12
|
+
with cls._lock:
|
|
13
|
+
if not cls._instance:
|
|
14
|
+
cls._instance = super(BackendManager, cls).__new__(cls)
|
|
15
|
+
return cls._instance
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
if hasattr(self, '_initialized') and self._initialized:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
self.backends = COMFYUI_BACKENDS
|
|
22
|
+
self.active_backend_name = "default"
|
|
23
|
+
self._initialized = True
|
|
24
|
+
print(f"[BackendManager] Initialized with default backend '{self.active_backend_name}'.")
|
|
25
|
+
|
|
26
|
+
def get_active_backend_url(self):
|
|
27
|
+
return self.backends.get(self.active_backend_name)
|
|
28
|
+
|
|
29
|
+
def get_all_backend_urls(self):
|
|
30
|
+
return list(self.backends.values())
|
|
31
|
+
|
|
32
|
+
def _free_backend_memory(self, backend_name, backend_url):
|
|
33
|
+
try:
|
|
34
|
+
print(f"[BackendManager] Sending /free request to {backend_name} ({backend_url})...")
|
|
35
|
+
response = requests.post(
|
|
36
|
+
f"{backend_url}/free",
|
|
37
|
+
json={"unload_models": True, "free_memory": True},
|
|
38
|
+
timeout=20
|
|
39
|
+
)
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
print(f"[BackendManager] Successfully freed memory for {backend_name}.")
|
|
42
|
+
except requests.exceptions.RequestException as e:
|
|
43
|
+
print(f"[BackendManager] Warning: Could not free memory for backend '{backend_name}'. "
|
|
44
|
+
f"Is the backend running and does it support the /free endpoint? Error: {e}")
|
|
45
|
+
|
|
46
|
+
def switch_backend(self, target_backend_name: str):
|
|
47
|
+
if target_backend_name not in self.backends:
|
|
48
|
+
print(f"[BackendManager] Error: Attempted to switch to an unknown backend '{target_backend_name}'. "
|
|
49
|
+
f"Falling back to 'default'.")
|
|
50
|
+
target_backend_name = "default"
|
|
51
|
+
|
|
52
|
+
if self.active_backend_name == target_backend_name:
|
|
53
|
+
print(f"[BackendManager] Backend '{target_backend_name}' is already active. No switch needed.")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
print(f"[BackendManager] Switching backend from '{self.active_backend_name}' to '{target_backend_name}'...")
|
|
57
|
+
|
|
58
|
+
inactive_backends = {name: url for name, url in self.backends.items() if name != target_backend_name}
|
|
59
|
+
|
|
60
|
+
if inactive_backends:
|
|
61
|
+
print(f"[BackendManager] Freeing up resources on {len(inactive_backends)} inactive backend(s)...")
|
|
62
|
+
with ThreadPoolExecutor(max_workers=len(inactive_backends)) as executor:
|
|
63
|
+
futures = [executor.submit(self._free_backend_memory, name, url) for name, url in inactive_backends.items()]
|
|
64
|
+
for future in futures:
|
|
65
|
+
future.result()
|
|
66
|
+
|
|
67
|
+
self.active_backend_name = target_backend_name
|
|
68
|
+
print(f"[BackendManager] Switch complete. Active backend is now '{self.active_backend_name}'.")
|
|
69
|
+
|
|
70
|
+
backend_manager = BackendManager()
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
def inject(assembler, chain_definition, chain_items):
|
|
2
|
+
if not chain_items:
|
|
3
|
+
return
|
|
4
|
+
|
|
5
|
+
ksampler_names = chain_definition.get('ksampler_node', 'ksampler')
|
|
6
|
+
if not isinstance(ksampler_names, list):
|
|
7
|
+
ksampler_names = [ksampler_names]
|
|
8
|
+
|
|
9
|
+
nodes_injected = 0
|
|
10
|
+
for ksampler_name in ksampler_names:
|
|
11
|
+
if ksampler_name not in assembler.node_map:
|
|
12
|
+
print(f"Warning: [EasyCache Injector] KSampler node '{ksampler_name}' not found. Skipping.")
|
|
13
|
+
continue
|
|
14
|
+
|
|
15
|
+
ksampler_id = assembler.node_map[ksampler_name]
|
|
16
|
+
|
|
17
|
+
if 'model' not in assembler.workflow[ksampler_id]['inputs']:
|
|
18
|
+
print(f"Warning: [EasyCache Injector] KSampler node '{ksampler_name}' is missing 'model' input. Skipping.")
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
|
|
22
|
+
|
|
23
|
+
easycache_id = assembler._get_unique_id()
|
|
24
|
+
easycache_node = assembler._get_node_template_from_api("EasyCache")
|
|
25
|
+
|
|
26
|
+
easycache_node['inputs']['reuse_threshold'] = chain_definition.get('reuse_threshold', 0.2)
|
|
27
|
+
easycache_node['inputs']['start_percent'] = chain_definition.get('start_percent', 0.15)
|
|
28
|
+
easycache_node['inputs']['end_percent'] = chain_definition.get('end_percent', 0.95)
|
|
29
|
+
easycache_node['inputs']['verbose'] = chain_definition.get('verbose', False)
|
|
30
|
+
|
|
31
|
+
easycache_node['inputs']['model'] = current_model_connection
|
|
32
|
+
assembler.workflow[easycache_id] = easycache_node
|
|
33
|
+
|
|
34
|
+
assembler.workflow[ksampler_id]['inputs']['model'] = [easycache_id, 0]
|
|
35
|
+
|
|
36
|
+
print(f"EasyCache injector applied. KSampler '{ksampler_name}' model input re-routed through EasyCache node.")
|
|
37
|
+
nodes_injected += 1
|
|
38
|
+
|
|
39
|
+
if nodes_injected > 0:
|
|
40
|
+
print(f"EasyCache injector finished. Applied to {nodes_injected} KSampler node(s).")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
|
|
3
|
+
def inject(assembler, chain_definition, chain_items):
|
|
4
|
+
if not chain_items:
|
|
5
|
+
return
|
|
6
|
+
|
|
7
|
+
start_node_name = chain_definition.get('start')
|
|
8
|
+
start_node_id = None
|
|
9
|
+
if start_node_name:
|
|
10
|
+
if start_node_name not in assembler.node_map:
|
|
11
|
+
print(f"Warning: Start node '{start_node_name}' for dynamic LoRA chain not found. Skipping chain.")
|
|
12
|
+
return
|
|
13
|
+
start_node_id = assembler.node_map[start_node_name]
|
|
14
|
+
|
|
15
|
+
output_map = chain_definition.get('output_map', {})
|
|
16
|
+
current_connections = {}
|
|
17
|
+
for key, type_name in output_map.items():
|
|
18
|
+
if ':' in str(key):
|
|
19
|
+
node_name, idx_str = key.split(':')
|
|
20
|
+
if node_name not in assembler.node_map:
|
|
21
|
+
print(f"Warning: Node '{node_name}' in chain's output_map not found. Skipping.")
|
|
22
|
+
continue
|
|
23
|
+
node_id = assembler.node_map[node_name]
|
|
24
|
+
start_output_idx = int(idx_str)
|
|
25
|
+
current_connections[type_name] = [node_id, start_output_idx]
|
|
26
|
+
elif start_node_id:
|
|
27
|
+
start_output_idx = int(key)
|
|
28
|
+
current_connections[type_name] = [start_node_id, start_output_idx]
|
|
29
|
+
else:
|
|
30
|
+
print(f"Warning: LoRA chain has no 'start' node defined, and an output_map key '{key}' is not in 'node:index' format. Skipping this connection.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
input_map = chain_definition.get('input_map', {})
|
|
34
|
+
chain_output_map = chain_definition.get('template_output_map', { "0": "model", "1": "clip" })
|
|
35
|
+
|
|
36
|
+
for item_data in chain_items:
|
|
37
|
+
template_name = chain_definition['template']
|
|
38
|
+
template = assembler._get_node_template_from_api(template_name)
|
|
39
|
+
node_data = deepcopy(template)
|
|
40
|
+
|
|
41
|
+
for param_name, value in item_data.items():
|
|
42
|
+
if param_name in node_data['inputs']:
|
|
43
|
+
node_data['inputs'][param_name] = value
|
|
44
|
+
|
|
45
|
+
for type_name, input_name in input_map.items():
|
|
46
|
+
if type_name in current_connections:
|
|
47
|
+
node_data['inputs'][input_name] = current_connections[type_name]
|
|
48
|
+
|
|
49
|
+
new_node_id = assembler._get_unique_id()
|
|
50
|
+
assembler.workflow[new_node_id] = node_data
|
|
51
|
+
|
|
52
|
+
for idx_str, type_name in chain_output_map.items():
|
|
53
|
+
current_connections[type_name] = [new_node_id, int(idx_str)]
|
|
54
|
+
|
|
55
|
+
end_input_map = chain_definition.get('end_input_map', {})
|
|
56
|
+
for type_name, targets in end_input_map.items():
|
|
57
|
+
if type_name in current_connections:
|
|
58
|
+
if not isinstance(targets, list):
|
|
59
|
+
targets = [targets]
|
|
60
|
+
|
|
61
|
+
for target_str in targets:
|
|
62
|
+
end_node_name, end_input_name = target_str.split(':')
|
|
63
|
+
if end_node_name in assembler.node_map:
|
|
64
|
+
end_node_id = assembler.node_map[end_node_name]
|
|
65
|
+
assembler.workflow[end_node_id]['inputs'][end_input_name] = current_connections[type_name]
|
|
66
|
+
else:
|
|
67
|
+
print(f"Warning: End node '{end_node_name}' for dynamic chain not found. Skipping connection.")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
|
|
3
|
+
def inject(assembler, chain_definition, chain_items):
|
|
4
|
+
if not chain_items:
|
|
5
|
+
return
|
|
6
|
+
|
|
7
|
+
output_map = chain_definition.get('output_map', {})
|
|
8
|
+
|
|
9
|
+
current_model_connection = None
|
|
10
|
+
for key, type_name in output_map.items():
|
|
11
|
+
if type_name == "model":
|
|
12
|
+
node_name, idx_str = key.split(':')
|
|
13
|
+
if node_name in assembler.node_map:
|
|
14
|
+
node_id = assembler.node_map[node_name]
|
|
15
|
+
current_model_connection = [node_id, int(idx_str)]
|
|
16
|
+
break
|
|
17
|
+
|
|
18
|
+
if not current_model_connection:
|
|
19
|
+
print(f"Warning: 'output_map' for LoRA (Model Only) chain must define a 'model' type from a valid node. Skipping.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
template_name = chain_definition.get('template')
|
|
23
|
+
if not template_name:
|
|
24
|
+
print(f"Warning: No 'template' defined for LoRA (Model Only) chain. Skipping.")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
for item_data in chain_items:
|
|
28
|
+
template = assembler._get_node_template_from_api(template_name)
|
|
29
|
+
node_data = deepcopy(template)
|
|
30
|
+
|
|
31
|
+
for param_name, value in item_data.items():
|
|
32
|
+
if param_name in node_data['inputs']:
|
|
33
|
+
node_data['inputs'][param_name] = value
|
|
34
|
+
|
|
35
|
+
node_data['inputs']['model'] = current_model_connection
|
|
36
|
+
|
|
37
|
+
new_node_id = assembler._get_unique_id()
|
|
38
|
+
assembler.workflow[new_node_id] = node_data
|
|
39
|
+
|
|
40
|
+
current_model_connection = [new_node_id, 0]
|
|
41
|
+
|
|
42
|
+
end_input_map = chain_definition.get('end_input_map', {})
|
|
43
|
+
model_targets = end_input_map.get('model', [])
|
|
44
|
+
if not isinstance(model_targets, list):
|
|
45
|
+
model_targets = [model_targets]
|
|
46
|
+
|
|
47
|
+
for target_str in model_targets:
|
|
48
|
+
try:
|
|
49
|
+
end_node_name, end_input_name = target_str.split(':')
|
|
50
|
+
if end_node_name in assembler.node_map:
|
|
51
|
+
end_node_id = assembler.node_map[end_node_name]
|
|
52
|
+
assembler.workflow[end_node_id]['inputs'][end_input_name] = current_model_connection
|
|
53
|
+
else:
|
|
54
|
+
print(f"Warning: End node '{end_node_name}' for dynamic LoRA (Model Only) chain not found. Skipping connection.")
|
|
55
|
+
except ValueError:
|
|
56
|
+
print(f"Warning: Invalid target format '{target_str}' in end_input_map. Skipping.")
|
|
57
|
+
|
|
58
|
+
if model_targets:
|
|
59
|
+
print(f"LoRA (Model Only) injector applied. Re-routed model through {len(chain_items)} LoRA(s).")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
def inject(assembler, chain_definition, chain_items):
|
|
2
|
+
if not chain_items:
|
|
3
|
+
return
|
|
4
|
+
|
|
5
|
+
guider_node_name = chain_definition.get('guider_node')
|
|
6
|
+
guider_target_inputs = chain_definition.get('guider_target_inputs', [])
|
|
7
|
+
start_connections_map = chain_definition.get('start_connections', {})
|
|
8
|
+
vae_node_name = chain_definition.get('vae_node', 'vae_loader')
|
|
9
|
+
|
|
10
|
+
if guider_node_name and guider_node_name in assembler.node_map and guider_target_inputs:
|
|
11
|
+
guider_id = assembler.node_map[guider_node_name]
|
|
12
|
+
if vae_node_name not in assembler.node_map:
|
|
13
|
+
print(f"Warning: VAE node '{vae_node_name}' not found for Guider chain. Skipping.")
|
|
14
|
+
return
|
|
15
|
+
vae_node_id = assembler.node_map[vae_node_name]
|
|
16
|
+
|
|
17
|
+
print(f"ReferenceLatent injector targeting DualCFGGuider node '{guider_node_name}'.")
|
|
18
|
+
|
|
19
|
+
current_connections = {}
|
|
20
|
+
for target_input in guider_target_inputs:
|
|
21
|
+
conn_str = start_connections_map.get(target_input)
|
|
22
|
+
if not conn_str:
|
|
23
|
+
print(f"Warning: No start connection defined for '{target_input}' in Guider chain. Skipping this input.")
|
|
24
|
+
continue
|
|
25
|
+
try:
|
|
26
|
+
node_name, idx_str = conn_str.split(':')
|
|
27
|
+
node_id = assembler.node_map[node_name]
|
|
28
|
+
current_connections[target_input] = [node_id, int(idx_str)]
|
|
29
|
+
except (ValueError, KeyError):
|
|
30
|
+
print(f"Warning: Invalid start connection '{conn_str}' for '{target_input}'. Skipping.")
|
|
31
|
+
|
|
32
|
+
encoded_latents = []
|
|
33
|
+
for i, img_filename in enumerate(chain_items):
|
|
34
|
+
load_id = assembler._get_unique_id()
|
|
35
|
+
load_node = assembler._get_node_template_from_api("LoadImage")
|
|
36
|
+
load_node['inputs']['image'] = img_filename
|
|
37
|
+
assembler.workflow[load_id] = load_node
|
|
38
|
+
|
|
39
|
+
scale_id = assembler._get_unique_id()
|
|
40
|
+
scale_node = assembler._get_node_template_from_api("ImageScaleToTotalPixels")
|
|
41
|
+
scale_node['inputs']['megapixels'] = 1.0
|
|
42
|
+
scale_node['inputs']['upscale_method'] = "lanczos"
|
|
43
|
+
scale_node['inputs']['image'] = [load_id, 0]
|
|
44
|
+
assembler.workflow[scale_id] = scale_node
|
|
45
|
+
|
|
46
|
+
vae_encode_id = assembler._get_unique_id()
|
|
47
|
+
vae_encode_node = assembler._get_node_template_from_api("VAEEncode")
|
|
48
|
+
vae_encode_node['inputs']['pixels'] = [scale_id, 0]
|
|
49
|
+
vae_encode_node['inputs']['vae'] = [vae_node_id, 0]
|
|
50
|
+
assembler.workflow[vae_encode_id] = vae_encode_node
|
|
51
|
+
encoded_latents.append([vae_encode_id, 0])
|
|
52
|
+
|
|
53
|
+
for target_input_name, start_connection in current_connections.items():
|
|
54
|
+
current_chain_head = start_connection
|
|
55
|
+
for i, latent_conn in enumerate(encoded_latents):
|
|
56
|
+
ref_latent_id = assembler._get_unique_id()
|
|
57
|
+
ref_latent_node = assembler._get_node_template_from_api("ReferenceLatent")
|
|
58
|
+
ref_latent_node['inputs']['conditioning'] = current_chain_head
|
|
59
|
+
ref_latent_node['inputs']['latent'] = latent_conn
|
|
60
|
+
ref_latent_node['_meta']['title'] = f"{target_input_name} RefLatent {i+1}"
|
|
61
|
+
assembler.workflow[ref_latent_id] = ref_latent_node
|
|
62
|
+
current_chain_head = [ref_latent_id, 0]
|
|
63
|
+
|
|
64
|
+
assembler.workflow[guider_id]['inputs'][target_input_name] = current_chain_head
|
|
65
|
+
print(f" - Input '{target_input_name}' of node '{guider_node_name}' re-routed through {len(chain_items)} reference images.")
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
flux_guidance_name = chain_definition.get('flux_guidance_node')
|
|
70
|
+
ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
|
|
71
|
+
|
|
72
|
+
if ksampler_name not in assembler.node_map:
|
|
73
|
+
print(f"Warning: KSampler node '{ksampler_name}' not found for ReferenceLatent chain. Skipping.")
|
|
74
|
+
return
|
|
75
|
+
if vae_node_name not in assembler.node_map:
|
|
76
|
+
print(f"Warning: VAE loader node '{vae_node_name}' not found for ReferenceLatent chain. Skipping.")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
ksampler_id = assembler.node_map[ksampler_name]
|
|
80
|
+
vae_node_id = assembler.node_map[vae_node_name]
|
|
81
|
+
|
|
82
|
+
pos_target_node_id = None
|
|
83
|
+
pos_target_input_name = None
|
|
84
|
+
if flux_guidance_name and flux_guidance_name in assembler.node_map:
|
|
85
|
+
flux_guidance_id = assembler.node_map[flux_guidance_name]
|
|
86
|
+
if 'conditioning' in assembler.workflow[flux_guidance_id]['inputs']:
|
|
87
|
+
pos_target_node_id = flux_guidance_id
|
|
88
|
+
pos_target_input_name = 'conditioning'
|
|
89
|
+
print(f"ReferenceLatent injector targeting FluxGuidance node '{flux_guidance_name}' for positive chain.")
|
|
90
|
+
|
|
91
|
+
if not pos_target_node_id:
|
|
92
|
+
if 'positive' in assembler.workflow[ksampler_id]['inputs']:
|
|
93
|
+
pos_target_node_id = ksampler_id
|
|
94
|
+
pos_target_input_name = 'positive'
|
|
95
|
+
print(f"ReferenceLatent injector targeting KSampler node '{ksampler_name}' for positive chain.")
|
|
96
|
+
else:
|
|
97
|
+
print(f"Warning: Could not find a valid positive injection point for ReferenceLatent chain. Skipping.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
current_pos_conditioning = assembler.workflow[pos_target_node_id]['inputs'][pos_target_input_name]
|
|
101
|
+
|
|
102
|
+
neg_target_node_id = ksampler_id
|
|
103
|
+
neg_target_input_name = 'negative'
|
|
104
|
+
if 'negative' not in assembler.workflow[neg_target_node_id]['inputs']:
|
|
105
|
+
print(f"Warning: KSampler node '{ksampler_name}' has no 'negative' input. Skipping negative ReferenceLatent chain.")
|
|
106
|
+
neg_target_node_id = None
|
|
107
|
+
|
|
108
|
+
current_neg_conditioning = None
|
|
109
|
+
if neg_target_node_id:
|
|
110
|
+
current_neg_conditioning = assembler.workflow[neg_target_node_id]['inputs'][neg_target_input_name]
|
|
111
|
+
|
|
112
|
+
for i, img_filename in enumerate(chain_items):
|
|
113
|
+
load_id = assembler._get_unique_id()
|
|
114
|
+
load_node = assembler._get_node_template_from_api("LoadImage")
|
|
115
|
+
load_node['inputs']['image'] = img_filename
|
|
116
|
+
load_node['_meta']['title'] = f"Load Reference Image {i+1}"
|
|
117
|
+
assembler.workflow[load_id] = load_node
|
|
118
|
+
|
|
119
|
+
scale_id = assembler._get_unique_id()
|
|
120
|
+
scale_node = assembler._get_node_template_from_api("ImageScaleToTotalPixels")
|
|
121
|
+
scale_node['inputs']['megapixels'] = 1.0
|
|
122
|
+
scale_node['inputs']['upscale_method'] = "lanczos"
|
|
123
|
+
scale_node['inputs']['image'] = [load_id, 0]
|
|
124
|
+
scale_node['_meta']['title'] = f"Scale Reference {i+1}"
|
|
125
|
+
assembler.workflow[scale_id] = scale_node
|
|
126
|
+
|
|
127
|
+
vae_encode_id = assembler._get_unique_id()
|
|
128
|
+
vae_encode_node = assembler._get_node_template_from_api("VAEEncode")
|
|
129
|
+
vae_encode_node['inputs']['pixels'] = [scale_id, 0]
|
|
130
|
+
vae_encode_node['inputs']['vae'] = [vae_node_id, 0]
|
|
131
|
+
vae_encode_node['_meta']['title'] = f"VAE Encode Reference {i+1}"
|
|
132
|
+
assembler.workflow[vae_encode_id] = vae_encode_node
|
|
133
|
+
|
|
134
|
+
latent_conn = [vae_encode_id, 0]
|
|
135
|
+
|
|
136
|
+
pos_ref_latent_id = assembler._get_unique_id()
|
|
137
|
+
pos_ref_latent_node = assembler._get_node_template_from_api("ReferenceLatent")
|
|
138
|
+
pos_ref_latent_node['inputs']['conditioning'] = current_pos_conditioning
|
|
139
|
+
pos_ref_latent_node['inputs']['latent'] = latent_conn
|
|
140
|
+
pos_ref_latent_node['_meta']['title'] = f"Positive ReferenceLatent {i+1}"
|
|
141
|
+
assembler.workflow[pos_ref_latent_id] = pos_ref_latent_node
|
|
142
|
+
current_pos_conditioning = [pos_ref_latent_id, 0]
|
|
143
|
+
|
|
144
|
+
if neg_target_node_id:
|
|
145
|
+
neg_ref_latent_id = assembler._get_unique_id()
|
|
146
|
+
neg_ref_latent_node = assembler._get_node_template_from_api("ReferenceLatent")
|
|
147
|
+
neg_ref_latent_node['inputs']['conditioning'] = current_neg_conditioning
|
|
148
|
+
neg_ref_latent_node['inputs']['latent'] = latent_conn
|
|
149
|
+
neg_ref_latent_node['_meta']['title'] = f"Negative ReferenceLatent {i+1}"
|
|
150
|
+
assembler.workflow[neg_ref_latent_id] = neg_ref_latent_node
|
|
151
|
+
current_neg_conditioning = [neg_ref_latent_id, 0]
|
|
152
|
+
|
|
153
|
+
assembler.workflow[pos_target_node_id]['inputs'][pos_target_input_name] = current_pos_conditioning
|
|
154
|
+
if neg_target_node_id:
|
|
155
|
+
assembler.workflow[neg_target_node_id]['inputs'][neg_target_input_name] = current_neg_conditioning
|
|
156
|
+
|
|
157
|
+
print(f"ReferenceLatent injector applied. Re-routed inputs through {len(chain_items)} reference images.")
|