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.
@@ -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.")