RioShiina commited on
Commit
2d31d6d
·
verified ·
1 Parent(s): e9e54d7

Add High-Level API/MCP Tools

Browse files
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -84,13 +84,15 @@ def main():
84
  print("--- Environment configured. Proceeding with module imports. ---")
85
  from ui.layout import build_ui
86
  from ui.events import attach_event_handlers
 
 
87
 
88
  print(f"✅ Working directory is stable: {os.getcwd()}")
89
 
90
  demo = build_ui(attach_event_handlers)
91
-
92
  print("--- Launching Gradio Interface ---")
93
- demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False)
94
 
95
 
96
  if __name__ == "__main__":
 
84
  print("--- Environment configured. Proceeding with module imports. ---")
85
  from ui.layout import build_ui
86
  from ui.events import attach_event_handlers
87
+ import mcp_tools as mcp
88
+ print(f"✅ Loaded MCP module with tools: {[fn.__name__ for fn in mcp.MCP_FUNCTIONS]}")
89
 
90
  print(f"✅ Working directory is stable: {os.getcwd()}")
91
 
92
  demo = build_ui(attach_event_handlers)
93
+
94
  print("--- Launching Gradio Interface ---")
95
+ demo.queue().launch(mcp_server=True)
96
 
97
 
98
  if __name__ == "__main__":
chain_injectors/__init__.py CHANGED
@@ -1,50 +1,50 @@
1
- import os
2
- import importlib
3
- import pkgutil
4
-
5
- def discover_injectors():
6
- injectors = {}
7
- package_dir = os.path.dirname(__file__)
8
-
9
- for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
10
- if is_pkg or module_name.startswith('_'):
11
- continue
12
-
13
- full_module_name = f"chain_injectors.{module_name}"
14
- try:
15
- module = importlib.import_module(full_module_name)
16
- if hasattr(module, 'inject') and callable(module.inject):
17
- feature_name = getattr(module, 'FEATURE_NAME', None)
18
- if not feature_name:
19
- feature_name = module_name[:-9] if module_name.endswith('_injector') else module_name
20
-
21
- chain_type = getattr(module, 'CHAIN_TYPE', None)
22
- if not chain_type:
23
- chain_type = f"dynamic_{feature_name}_chains"
24
-
25
- injectors[chain_type] = module.inject
26
- else:
27
- print(f"Warning: Module '{full_module_name}' does not have a callable 'inject' function.")
28
- except Exception as e:
29
- print(f"Error importing injector module '{full_module_name}': {e}")
30
-
31
- return injectors
32
-
33
- def get_registered_features():
34
- features = {}
35
- package_dir = os.path.dirname(__file__)
36
-
37
- for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
38
- if is_pkg or module_name.startswith('_'):
39
- continue
40
-
41
- feature_name = module_name[:-9] if module_name.endswith('_injector') else module_name
42
- full_module_name = f"chain_injectors.{module_name}"
43
- chain_type = f"dynamic_{feature_name}_chains"
44
-
45
- features[feature_name] = {
46
- 'module': full_module_name,
47
- 'chain_type': chain_type
48
- }
49
-
50
- return features
 
1
+ import os
2
+ import importlib
3
+ import pkgutil
4
+
5
+ def discover_injectors():
6
+ injectors = {}
7
+ package_dir = os.path.dirname(__file__)
8
+
9
+ for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
10
+ if is_pkg or module_name.startswith('_'):
11
+ continue
12
+
13
+ full_module_name = f"chain_injectors.{module_name}"
14
+ try:
15
+ module = importlib.import_module(full_module_name)
16
+ if hasattr(module, 'inject') and callable(module.inject):
17
+ feature_name = getattr(module, 'FEATURE_NAME', None)
18
+ if not feature_name:
19
+ feature_name = module_name[:-9] if module_name.endswith('_injector') else module_name
20
+
21
+ chain_type = getattr(module, 'CHAIN_TYPE', None)
22
+ if not chain_type:
23
+ chain_type = f"dynamic_{feature_name}_chains"
24
+
25
+ injectors[chain_type] = module.inject
26
+ else:
27
+ print(f"Warning: Module '{full_module_name}' does not have a callable 'inject' function.")
28
+ except Exception as e:
29
+ print(f"Error importing injector module '{full_module_name}': {e}")
30
+
31
+ return injectors
32
+
33
+ def get_registered_features():
34
+ features = {}
35
+ package_dir = os.path.dirname(__file__)
36
+
37
+ for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
38
+ if is_pkg or module_name.startswith('_'):
39
+ continue
40
+
41
+ feature_name = module_name[:-9] if module_name.endswith('_injector') else module_name
42
+ full_module_name = f"chain_injectors.{module_name}"
43
+ chain_type = f"dynamic_{feature_name}_chains"
44
+
45
+ features[feature_name] = {
46
+ 'module': full_module_name,
47
+ 'chain_type': chain_type
48
+ }
49
+
50
+ return features
chain_injectors/boogu_image_edit_injector.py CHANGED
@@ -1,73 +1,73 @@
1
- def create_node(assembler, class_type, title):
2
- try:
3
- node = assembler._get_node_template(class_type)
4
- except Exception:
5
- node = {
6
- "inputs": {},
7
- "class_type": class_type,
8
- "_meta": {"title": title}
9
- }
10
- node['_meta']['title'] = title
11
- return node
12
-
13
- def inject(assembler, chain_definition, chain_items):
14
- if not chain_items:
15
- return
16
-
17
- valid_images = []
18
- for item in chain_items:
19
- if not item:
20
- continue
21
- img_path = item
22
- if isinstance(item, dict):
23
- img_path = item.get('image') or item.get('filename') or item.get('path')
24
- if img_path:
25
- valid_images.append(img_path)
26
-
27
- if not valid_images:
28
- return
29
-
30
- valid_images = valid_images[:10]
31
-
32
- boogu_prompt_name = chain_definition.get('boogu_prompt_node', 'boogu_prompt')
33
- vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
34
-
35
- boogu_prompt_id = assembler.node_map.get(boogu_prompt_name)
36
- if not boogu_prompt_id or boogu_prompt_id not in assembler.workflow:
37
- for node_id, node in assembler.workflow.items():
38
- if isinstance(node, dict) and node.get('class_type') == 'TextEncodeBooguEdit':
39
- boogu_prompt_id = node_id
40
- break
41
-
42
- if not boogu_prompt_id:
43
- print(f"Warning: Target node '{boogu_prompt_name}' (TextEncodeBooguEdit) for Boogu Edit chain not found. Skipping.")
44
- return
45
-
46
- vae_id = assembler.node_map.get(vae_loader_name)
47
- if not vae_id:
48
- for node_id, node in assembler.workflow.items():
49
- if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
50
- vae_id = node_id
51
- break
52
-
53
- if vae_id:
54
- assembler.workflow[boogu_prompt_id]['inputs']['vae'] = [vae_id, 0]
55
-
56
- for i, img_filename in enumerate(valid_images):
57
- load_id = assembler._get_unique_id()
58
- load_node = create_node(assembler, "LoadImage", f"Load Reference Image {i+1}")
59
- load_node['inputs']['image'] = img_filename
60
- assembler.workflow[load_id] = load_node
61
-
62
- scale_id = assembler._get_unique_id()
63
- scale_node = create_node(assembler, "ImageScaleToTotalPixels", f"Scale Reference {i+1}")
64
- scale_node['inputs']['upscale_method'] = "nearest-exact"
65
- scale_node['inputs']['megapixels'] = 1
66
- scale_node['inputs']['resolution_steps'] = 1
67
- scale_node['inputs']['image'] = [load_id, 0]
68
- assembler.workflow[scale_id] = scale_node
69
-
70
- image_key = f"images.image_{i+1}"
71
- assembler.workflow[boogu_prompt_id]['inputs'][image_key] = [scale_id, 0]
72
-
73
- print(f"Boogu Edit injector applied with {len(valid_images)} reference image(s). Connected VAE dynamically.")
 
1
+ def create_node(assembler, class_type, title):
2
+ try:
3
+ node = assembler._get_node_template(class_type)
4
+ except Exception:
5
+ node = {
6
+ "inputs": {},
7
+ "class_type": class_type,
8
+ "_meta": {"title": title}
9
+ }
10
+ node['_meta']['title'] = title
11
+ return node
12
+
13
+ def inject(assembler, chain_definition, chain_items):
14
+ if not chain_items:
15
+ return
16
+
17
+ valid_images = []
18
+ for item in chain_items:
19
+ if not item:
20
+ continue
21
+ img_path = item
22
+ if isinstance(item, dict):
23
+ img_path = item.get('image') or item.get('filename') or item.get('path')
24
+ if img_path:
25
+ valid_images.append(img_path)
26
+
27
+ if not valid_images:
28
+ return
29
+
30
+ valid_images = valid_images[:10]
31
+
32
+ boogu_prompt_name = chain_definition.get('boogu_prompt_node', 'boogu_prompt')
33
+ vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
34
+
35
+ boogu_prompt_id = assembler.node_map.get(boogu_prompt_name)
36
+ if not boogu_prompt_id or boogu_prompt_id not in assembler.workflow:
37
+ for node_id, node in assembler.workflow.items():
38
+ if isinstance(node, dict) and node.get('class_type') == 'TextEncodeBooguEdit':
39
+ boogu_prompt_id = node_id
40
+ break
41
+
42
+ if not boogu_prompt_id:
43
+ print(f"Warning: Target node '{boogu_prompt_name}' (TextEncodeBooguEdit) for Boogu Edit chain not found. Skipping.")
44
+ return
45
+
46
+ vae_id = assembler.node_map.get(vae_loader_name)
47
+ if not vae_id:
48
+ for node_id, node in assembler.workflow.items():
49
+ if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
50
+ vae_id = node_id
51
+ break
52
+
53
+ if vae_id:
54
+ assembler.workflow[boogu_prompt_id]['inputs']['vae'] = [vae_id, 0]
55
+
56
+ for i, img_filename in enumerate(valid_images):
57
+ load_id = assembler._get_unique_id()
58
+ load_node = create_node(assembler, "LoadImage", f"Load Reference Image {i+1}")
59
+ load_node['inputs']['image'] = img_filename
60
+ assembler.workflow[load_id] = load_node
61
+
62
+ scale_id = assembler._get_unique_id()
63
+ scale_node = create_node(assembler, "ImageScaleToTotalPixels", f"Scale Reference {i+1}")
64
+ scale_node['inputs']['upscale_method'] = "nearest-exact"
65
+ scale_node['inputs']['megapixels'] = 1
66
+ scale_node['inputs']['resolution_steps'] = 1
67
+ scale_node['inputs']['image'] = [load_id, 0]
68
+ assembler.workflow[scale_id] = scale_node
69
+
70
+ image_key = f"images.image_{i+1}"
71
+ assembler.workflow[boogu_prompt_id]['inputs'][image_key] = [scale_id, 0]
72
+
73
+ print(f"Boogu Edit injector applied with {len(valid_images)} reference image(s). Connected VAE dynamically.")
chain_injectors/joyai_image_injector.py CHANGED
@@ -1,63 +1,63 @@
1
- import os
2
-
3
- def inject(assembler, chain_definition, chain_items):
4
- if not chain_items:
5
- return
6
-
7
- valid_images = []
8
- for item in chain_items:
9
- if not item:
10
- continue
11
- img_path = item
12
- if isinstance(item, dict):
13
- img_path = item.get('image') or item.get('filename') or item.get('path')
14
- if img_path:
15
- valid_images.append(img_path)
16
-
17
- if not valid_images:
18
- return
19
-
20
- valid_images = valid_images[:2]
21
-
22
- pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
23
- neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
24
- vae_node_name = chain_definition.get('vae_node', 'vae_loader')
25
-
26
- if pos_prompt_name not in assembler.node_map:
27
- print(f"Warning: Positive prompt node '{pos_prompt_name}' not found for JoyAI Reference chain. Skipping.")
28
- return
29
-
30
- if vae_node_name not in assembler.node_map:
31
- print(f"Warning: VAE loader node '{vae_node_name}' not found for JoyAI Reference chain. Skipping.")
32
- return
33
-
34
- pos_prompt_id = assembler.node_map[pos_prompt_name]
35
- neg_prompt_id = assembler.node_map.get(neg_prompt_name)
36
- vae_node_id = assembler.node_map[vae_node_name]
37
-
38
- assembler.workflow[pos_prompt_id]['inputs']['vae'] = [vae_node_id, 0]
39
- if neg_prompt_id and neg_prompt_id in assembler.workflow:
40
- assembler.workflow[neg_prompt_id]['inputs']['vae'] = [vae_node_id, 0]
41
-
42
- for i, img_filename in enumerate(valid_images):
43
- load_id = assembler._get_unique_id()
44
- load_node = assembler._get_node_template("LoadImage")
45
- load_node['inputs']['image'] = img_filename
46
- load_node['_meta']['title'] = f"Load Reference Image {i+1}"
47
- assembler.workflow[load_id] = load_node
48
-
49
- scale_id = assembler._get_unique_id()
50
- scale_node = assembler._get_node_template("ImageScaleToTotalPixels")
51
- scale_node['inputs']['megapixels'] = 1.0
52
- scale_node['inputs']['upscale_method'] = "nearest-exact"
53
- scale_node['inputs']['resolution_steps'] = 1
54
- scale_node['inputs']['image'] = [load_id, 0]
55
- scale_node['_meta']['title'] = f"Scale Reference {i+1}"
56
- assembler.workflow[scale_id] = scale_node
57
-
58
- input_key = f"images.image{i}"
59
- assembler.workflow[pos_prompt_id]['inputs'][input_key] = [scale_id, 0]
60
- if neg_prompt_id and neg_prompt_id in assembler.workflow:
61
- assembler.workflow[neg_prompt_id]['inputs'][input_key] = [scale_id, 0]
62
-
63
- print(f"JoyAI Reference injector applied. Injected {len(valid_images)} reference images to JoyAI text encoding nodes.")
 
1
+ import os
2
+
3
+ def inject(assembler, chain_definition, chain_items):
4
+ if not chain_items:
5
+ return
6
+
7
+ valid_images = []
8
+ for item in chain_items:
9
+ if not item:
10
+ continue
11
+ img_path = item
12
+ if isinstance(item, dict):
13
+ img_path = item.get('image') or item.get('filename') or item.get('path')
14
+ if img_path:
15
+ valid_images.append(img_path)
16
+
17
+ if not valid_images:
18
+ return
19
+
20
+ valid_images = valid_images[:2]
21
+
22
+ pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
23
+ neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
24
+ vae_node_name = chain_definition.get('vae_node', 'vae_loader')
25
+
26
+ if pos_prompt_name not in assembler.node_map:
27
+ print(f"Warning: Positive prompt node '{pos_prompt_name}' not found for JoyAI Reference chain. Skipping.")
28
+ return
29
+
30
+ if vae_node_name not in assembler.node_map:
31
+ print(f"Warning: VAE loader node '{vae_node_name}' not found for JoyAI Reference chain. Skipping.")
32
+ return
33
+
34
+ pos_prompt_id = assembler.node_map[pos_prompt_name]
35
+ neg_prompt_id = assembler.node_map.get(neg_prompt_name)
36
+ vae_node_id = assembler.node_map[vae_node_name]
37
+
38
+ assembler.workflow[pos_prompt_id]['inputs']['vae'] = [vae_node_id, 0]
39
+ if neg_prompt_id and neg_prompt_id in assembler.workflow:
40
+ assembler.workflow[neg_prompt_id]['inputs']['vae'] = [vae_node_id, 0]
41
+
42
+ for i, img_filename in enumerate(valid_images):
43
+ load_id = assembler._get_unique_id()
44
+ load_node = assembler._get_node_template("LoadImage")
45
+ load_node['inputs']['image'] = img_filename
46
+ load_node['_meta']['title'] = f"Load Reference Image {i+1}"
47
+ assembler.workflow[load_id] = load_node
48
+
49
+ scale_id = assembler._get_unique_id()
50
+ scale_node = assembler._get_node_template("ImageScaleToTotalPixels")
51
+ scale_node['inputs']['megapixels'] = 1.0
52
+ scale_node['inputs']['upscale_method'] = "nearest-exact"
53
+ scale_node['inputs']['resolution_steps'] = 1
54
+ scale_node['inputs']['image'] = [load_id, 0]
55
+ scale_node['_meta']['title'] = f"Scale Reference {i+1}"
56
+ assembler.workflow[scale_id] = scale_node
57
+
58
+ input_key = f"images.image{i}"
59
+ assembler.workflow[pos_prompt_id]['inputs'][input_key] = [scale_id, 0]
60
+ if neg_prompt_id and neg_prompt_id in assembler.workflow:
61
+ assembler.workflow[neg_prompt_id]['inputs'][input_key] = [scale_id, 0]
62
+
63
+ print(f"JoyAI Reference injector applied. Injected {len(valid_images)} reference images to JoyAI text encoding nodes.")
chain_injectors/krea2_identity_edit_injector.py CHANGED
@@ -1,173 +1,173 @@
1
- import os
2
- from utils.app_utils import ensure_file_downloaded
3
-
4
- def inject(assembler, chain_definition, chain_items):
5
- if not chain_items:
6
- return
7
-
8
- valid_images = []
9
- for item in chain_items:
10
- if not item:
11
- continue
12
- img_path = item
13
- if isinstance(item, dict):
14
- img_path = item.get('image') or item.get('filename') or item.get('path')
15
- if img_path:
16
- valid_images.append(img_path)
17
-
18
- if not valid_images:
19
- return
20
-
21
- valid_images = valid_images[:2]
22
-
23
- lora_filename = "krea2_identity_edit_v1_2.safetensors"
24
- try:
25
- ensure_file_downloaded(lora_filename)
26
- except Exception as e:
27
- print(f"Warning: Failed to ensure '{lora_filename}' downloaded: {e}")
28
-
29
- ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
30
- pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
31
- neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
32
- clip_loader_name = chain_definition.get('clip_loader_node', 'clip_loader')
33
- vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
34
-
35
- if ksampler_name not in assembler.node_map:
36
- print(f"Warning: Target node '{ksampler_name}' for Krea2 Identity Edit chain not found. Skipping.")
37
- return
38
-
39
- ksampler_id = assembler.node_map[ksampler_name]
40
-
41
- if 'model' not in assembler.workflow[ksampler_id]['inputs']:
42
- print(f"Warning: KSampler node '{ksampler_name}' is missing 'model' input. Skipping.")
43
- return
44
-
45
- latent_connection = assembler.workflow[ksampler_id]['inputs'].get('latent_image')
46
- if not latent_connection:
47
- print(f"Warning: KSampler node '{ksampler_name}' is missing 'latent_image' input. Skipping.")
48
- return
49
-
50
- current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
51
-
52
- vae_connection = None
53
- if vae_loader_name in assembler.node_map:
54
- vae_connection = [assembler.node_map[vae_loader_name], 0]
55
-
56
- clip_connection = None
57
- if clip_loader_name in assembler.node_map:
58
- clip_connection = [assembler.node_map[clip_loader_name], 0]
59
- elif pos_prompt_name in assembler.node_map:
60
- pos_id = assembler.node_map[pos_prompt_name]
61
- clip_connection = assembler.workflow[pos_id]['inputs'].get('clip')
62
-
63
- lora_loader_id = assembler._get_unique_id()
64
- lora_loader_node = assembler._get_node_template("LoraLoaderModelOnly")
65
- lora_loader_node['inputs']['lora_name'] = lora_filename
66
- lora_loader_node['inputs']['strength_model'] = 1.0
67
- lora_loader_node['inputs']['model'] = current_model_connection
68
- lora_loader_node['_meta']['title'] = "Load LoRA (Krea2 Identity Edit)"
69
- assembler.workflow[lora_loader_id] = lora_loader_node
70
-
71
- image_ids = []
72
- vae_encode_ids = []
73
-
74
- for i, img_filename in enumerate(valid_images):
75
- load_id = assembler._get_unique_id()
76
- load_node = assembler._get_node_template("LoadImage")
77
- load_node['inputs']['image'] = img_filename
78
- load_node['_meta']['title'] = f"Load Image (Ref {i+1})"
79
- assembler.workflow[load_id] = load_node
80
- image_ids.append(load_id)
81
-
82
- vae_enc_id = assembler._get_unique_id()
83
- vae_enc_node = assembler._get_node_template("VAEEncode")
84
- vae_enc_node['inputs']['pixels'] = [load_id, 0]
85
- if vae_connection:
86
- vae_enc_node['inputs']['vae'] = vae_connection
87
- vae_enc_node['_meta']['title'] = f"VAE Encode (Ref {i+1})"
88
- assembler.workflow[vae_enc_id] = vae_enc_node
89
- vae_encode_ids.append(vae_enc_id)
90
-
91
- patch_id = assembler._get_unique_id()
92
- patch_node = assembler._get_node_template("Krea2EditModelPatch")
93
- patch_node['inputs']['ref_boost'] = 4
94
- patch_node['inputs']['ref_boost_a'] = 1
95
- patch_node['inputs']['fit_mode'] = "fit"
96
- patch_node['inputs']['model'] = [lora_loader_id, 0]
97
- patch_node['inputs']['source_latent'] = [vae_encode_ids[0], 0]
98
- if vae_connection:
99
- patch_node['inputs']['vae'] = vae_connection
100
- patch_node['inputs']['source_image'] = [image_ids[0], 0]
101
- patch_node['inputs']['target_latent'] = latent_connection
102
-
103
- if len(valid_images) > 1:
104
- patch_node['inputs']['source_latent_b'] = [vae_encode_ids[1], 0]
105
- patch_node['inputs']['source_image_b'] = [image_ids[1], 0]
106
-
107
- patch_node['_meta']['title'] = "Krea2 Edit (source patch)"
108
- assembler.workflow[patch_id] = patch_node
109
-
110
- assembler.workflow[ksampler_id]['inputs']['model'] = [patch_id, 0]
111
-
112
- pos_prompt_id = assembler.node_map.get(pos_prompt_name)
113
- neg_prompt_id = assembler.node_map.get(neg_prompt_name)
114
-
115
- pos_text = ""
116
- if pos_prompt_id and pos_prompt_id in assembler.workflow:
117
- pos_text = assembler.workflow[pos_prompt_id]['inputs'].get('text', '')
118
- elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
119
- pos_text = assembler.ui_values.get('positive_prompt') or assembler.ui_values.get('prompt') or ''
120
-
121
- if not pos_text:
122
- for node_id, node in assembler.workflow.items():
123
- if isinstance(node, dict):
124
- cls = node.get('class_type', '')
125
- if cls in ['Krea2EditGroundedEncode', 'TextEncodeQwenImageEditPlus', 'CLIPTextEncode']:
126
- t = node.get('inputs', {}).get('prompt') or node.get('inputs', {}).get('text')
127
- if t:
128
- pos_text = t
129
- break
130
-
131
- neg_text = ""
132
- if neg_prompt_id and neg_prompt_id in assembler.workflow:
133
- neg_text = assembler.workflow[neg_prompt_id]['inputs'].get('text', '')
134
- elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
135
- neg_text = assembler.ui_values.get('negative_prompt') or assembler.ui_values.get('neg_prompt') or ''
136
-
137
- pos_grounded_id = assembler._get_unique_id()
138
- pos_grounded_node = assembler._get_node_template("Krea2EditGroundedEncode")
139
- pos_grounded_node['inputs']['prompt'] = pos_text
140
- pos_grounded_node['inputs']['grounding_px'] = 768
141
- pos_grounded_node['inputs']['system_prompt'] = ""
142
- if clip_connection:
143
- pos_grounded_node['inputs']['clip'] = clip_connection
144
- pos_grounded_node['inputs']['image'] = [image_ids[0], 0]
145
- if len(valid_images) > 1:
146
- pos_grounded_node['inputs']['image_b'] = [image_ids[1], 0]
147
- pos_grounded_node['_meta']['title'] = "Krea2 Edit (grounded encode positive)"
148
- assembler.workflow[pos_grounded_id] = pos_grounded_node
149
-
150
- assembler.workflow[ksampler_id]['inputs']['positive'] = [pos_grounded_id, 0]
151
-
152
- neg_grounded_id = assembler._get_unique_id()
153
- neg_grounded_node = assembler._get_node_template("Krea2EditGroundedEncode")
154
- neg_grounded_node['inputs']['prompt'] = neg_text
155
- neg_grounded_node['inputs']['grounding_px'] = 768
156
- neg_grounded_node['inputs']['system_prompt'] = ""
157
- if clip_connection:
158
- neg_grounded_node['inputs']['clip'] = clip_connection
159
- neg_grounded_node['inputs']['image'] = [image_ids[0], 0]
160
- if len(valid_images) > 1:
161
- neg_grounded_node['inputs']['image_b'] = [image_ids[1], 0]
162
- neg_grounded_node['_meta']['title'] = "Krea2 Edit (grounded encode negative)"
163
- assembler.workflow[neg_grounded_id] = neg_grounded_node
164
-
165
- assembler.workflow[ksampler_id]['inputs']['negative'] = [neg_grounded_id, 0]
166
-
167
- if pos_prompt_id and pos_prompt_id in assembler.workflow:
168
- del assembler.workflow[pos_prompt_id]
169
-
170
- if neg_prompt_id and neg_prompt_id in assembler.workflow:
171
- del assembler.workflow[neg_prompt_id]
172
-
173
- print(f"Krea2 Identity Edit injector applied with {len(valid_images)} reference image(s). Original CLIPTextEncode nodes removed.")
 
1
+ import os
2
+ from utils.app_utils import ensure_file_downloaded
3
+
4
+ def inject(assembler, chain_definition, chain_items):
5
+ if not chain_items:
6
+ return
7
+
8
+ valid_images = []
9
+ for item in chain_items:
10
+ if not item:
11
+ continue
12
+ img_path = item
13
+ if isinstance(item, dict):
14
+ img_path = item.get('image') or item.get('filename') or item.get('path')
15
+ if img_path:
16
+ valid_images.append(img_path)
17
+
18
+ if not valid_images:
19
+ return
20
+
21
+ valid_images = valid_images[:2]
22
+
23
+ lora_filename = "krea2_identity_edit_v1_2.safetensors"
24
+ try:
25
+ ensure_file_downloaded(lora_filename)
26
+ except Exception as e:
27
+ print(f"Warning: Failed to ensure '{lora_filename}' downloaded: {e}")
28
+
29
+ ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
30
+ pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
31
+ neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
32
+ clip_loader_name = chain_definition.get('clip_loader_node', 'clip_loader')
33
+ vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
34
+
35
+ if ksampler_name not in assembler.node_map:
36
+ print(f"Warning: Target node '{ksampler_name}' for Krea2 Identity Edit chain not found. Skipping.")
37
+ return
38
+
39
+ ksampler_id = assembler.node_map[ksampler_name]
40
+
41
+ if 'model' not in assembler.workflow[ksampler_id]['inputs']:
42
+ print(f"Warning: KSampler node '{ksampler_name}' is missing 'model' input. Skipping.")
43
+ return
44
+
45
+ latent_connection = assembler.workflow[ksampler_id]['inputs'].get('latent_image')
46
+ if not latent_connection:
47
+ print(f"Warning: KSampler node '{ksampler_name}' is missing 'latent_image' input. Skipping.")
48
+ return
49
+
50
+ current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
51
+
52
+ vae_connection = None
53
+ if vae_loader_name in assembler.node_map:
54
+ vae_connection = [assembler.node_map[vae_loader_name], 0]
55
+
56
+ clip_connection = None
57
+ if clip_loader_name in assembler.node_map:
58
+ clip_connection = [assembler.node_map[clip_loader_name], 0]
59
+ elif pos_prompt_name in assembler.node_map:
60
+ pos_id = assembler.node_map[pos_prompt_name]
61
+ clip_connection = assembler.workflow[pos_id]['inputs'].get('clip')
62
+
63
+ lora_loader_id = assembler._get_unique_id()
64
+ lora_loader_node = assembler._get_node_template("LoraLoaderModelOnly")
65
+ lora_loader_node['inputs']['lora_name'] = lora_filename
66
+ lora_loader_node['inputs']['strength_model'] = 1.0
67
+ lora_loader_node['inputs']['model'] = current_model_connection
68
+ lora_loader_node['_meta']['title'] = "Load LoRA (Krea2 Identity Edit)"
69
+ assembler.workflow[lora_loader_id] = lora_loader_node
70
+
71
+ image_ids = []
72
+ vae_encode_ids = []
73
+
74
+ for i, img_filename in enumerate(valid_images):
75
+ load_id = assembler._get_unique_id()
76
+ load_node = assembler._get_node_template("LoadImage")
77
+ load_node['inputs']['image'] = img_filename
78
+ load_node['_meta']['title'] = f"Load Image (Ref {i+1})"
79
+ assembler.workflow[load_id] = load_node
80
+ image_ids.append(load_id)
81
+
82
+ vae_enc_id = assembler._get_unique_id()
83
+ vae_enc_node = assembler._get_node_template("VAEEncode")
84
+ vae_enc_node['inputs']['pixels'] = [load_id, 0]
85
+ if vae_connection:
86
+ vae_enc_node['inputs']['vae'] = vae_connection
87
+ vae_enc_node['_meta']['title'] = f"VAE Encode (Ref {i+1})"
88
+ assembler.workflow[vae_enc_id] = vae_enc_node
89
+ vae_encode_ids.append(vae_enc_id)
90
+
91
+ patch_id = assembler._get_unique_id()
92
+ patch_node = assembler._get_node_template("Krea2EditModelPatch")
93
+ patch_node['inputs']['ref_boost'] = 4
94
+ patch_node['inputs']['ref_boost_a'] = 1
95
+ patch_node['inputs']['fit_mode'] = "fit"
96
+ patch_node['inputs']['model'] = [lora_loader_id, 0]
97
+ patch_node['inputs']['source_latent'] = [vae_encode_ids[0], 0]
98
+ if vae_connection:
99
+ patch_node['inputs']['vae'] = vae_connection
100
+ patch_node['inputs']['source_image'] = [image_ids[0], 0]
101
+ patch_node['inputs']['target_latent'] = latent_connection
102
+
103
+ if len(valid_images) > 1:
104
+ patch_node['inputs']['source_latent_b'] = [vae_encode_ids[1], 0]
105
+ patch_node['inputs']['source_image_b'] = [image_ids[1], 0]
106
+
107
+ patch_node['_meta']['title'] = "Krea2 Edit (source patch)"
108
+ assembler.workflow[patch_id] = patch_node
109
+
110
+ assembler.workflow[ksampler_id]['inputs']['model'] = [patch_id, 0]
111
+
112
+ pos_prompt_id = assembler.node_map.get(pos_prompt_name)
113
+ neg_prompt_id = assembler.node_map.get(neg_prompt_name)
114
+
115
+ pos_text = ""
116
+ if pos_prompt_id and pos_prompt_id in assembler.workflow:
117
+ pos_text = assembler.workflow[pos_prompt_id]['inputs'].get('text', '')
118
+ elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
119
+ pos_text = assembler.ui_values.get('positive_prompt') or assembler.ui_values.get('prompt') or ''
120
+
121
+ if not pos_text:
122
+ for node_id, node in assembler.workflow.items():
123
+ if isinstance(node, dict):
124
+ cls = node.get('class_type', '')
125
+ if cls in ['Krea2EditGroundedEncode', 'TextEncodeQwenImageEditPlus', 'CLIPTextEncode']:
126
+ t = node.get('inputs', {}).get('prompt') or node.get('inputs', {}).get('text')
127
+ if t:
128
+ pos_text = t
129
+ break
130
+
131
+ neg_text = ""
132
+ if neg_prompt_id and neg_prompt_id in assembler.workflow:
133
+ neg_text = assembler.workflow[neg_prompt_id]['inputs'].get('text', '')
134
+ elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
135
+ neg_text = assembler.ui_values.get('negative_prompt') or assembler.ui_values.get('neg_prompt') or ''
136
+
137
+ pos_grounded_id = assembler._get_unique_id()
138
+ pos_grounded_node = assembler._get_node_template("Krea2EditGroundedEncode")
139
+ pos_grounded_node['inputs']['prompt'] = pos_text
140
+ pos_grounded_node['inputs']['grounding_px'] = 768
141
+ pos_grounded_node['inputs']['system_prompt'] = ""
142
+ if clip_connection:
143
+ pos_grounded_node['inputs']['clip'] = clip_connection
144
+ pos_grounded_node['inputs']['image'] = [image_ids[0], 0]
145
+ if len(valid_images) > 1:
146
+ pos_grounded_node['inputs']['image_b'] = [image_ids[1], 0]
147
+ pos_grounded_node['_meta']['title'] = "Krea2 Edit (grounded encode positive)"
148
+ assembler.workflow[pos_grounded_id] = pos_grounded_node
149
+
150
+ assembler.workflow[ksampler_id]['inputs']['positive'] = [pos_grounded_id, 0]
151
+
152
+ neg_grounded_id = assembler._get_unique_id()
153
+ neg_grounded_node = assembler._get_node_template("Krea2EditGroundedEncode")
154
+ neg_grounded_node['inputs']['prompt'] = neg_text
155
+ neg_grounded_node['inputs']['grounding_px'] = 768
156
+ neg_grounded_node['inputs']['system_prompt'] = ""
157
+ if clip_connection:
158
+ neg_grounded_node['inputs']['clip'] = clip_connection
159
+ neg_grounded_node['inputs']['image'] = [image_ids[0], 0]
160
+ if len(valid_images) > 1:
161
+ neg_grounded_node['inputs']['image_b'] = [image_ids[1], 0]
162
+ neg_grounded_node['_meta']['title'] = "Krea2 Edit (grounded encode negative)"
163
+ assembler.workflow[neg_grounded_id] = neg_grounded_node
164
+
165
+ assembler.workflow[ksampler_id]['inputs']['negative'] = [neg_grounded_id, 0]
166
+
167
+ if pos_prompt_id and pos_prompt_id in assembler.workflow:
168
+ del assembler.workflow[pos_prompt_id]
169
+
170
+ if neg_prompt_id and neg_prompt_id in assembler.workflow:
171
+ del assembler.workflow[neg_prompt_id]
172
+
173
+ print(f"Krea2 Identity Edit injector applied with {len(valid_images)} reference image(s). Original CLIPTextEncode nodes removed.")
chain_injectors/krea2_style_reference_injector.py CHANGED
@@ -1,201 +1,168 @@
1
- import os
2
- from utils.app_utils import ensure_file_downloaded
3
-
4
- def create_node(assembler, class_type, title):
5
- try:
6
- node = assembler._get_node_template(class_type)
7
- except Exception:
8
- node = {
9
- "inputs": {},
10
- "class_type": class_type,
11
- "_meta": {"title": title}
12
- }
13
- node['_meta']['title'] = title
14
- return node
15
-
16
- def inject(assembler, chain_definition, chain_items):
17
- if not chain_items:
18
- return
19
-
20
- valid_images = []
21
- for item in chain_items:
22
- if not item:
23
- continue
24
- img_path = item
25
- if isinstance(item, dict):
26
- img_path = item.get('image') or item.get('filename') or item.get('path')
27
- if img_path:
28
- valid_images.append(img_path)
29
-
30
- if not valid_images:
31
- return
32
-
33
- valid_images = valid_images[:3]
34
-
35
- lora_filename = "krea2_style_reference.safetensors"
36
- try:
37
- ensure_file_downloaded(lora_filename)
38
- except Exception as e:
39
- print(f"Warning: Failed to ensure '{lora_filename}' downloaded: {e}")
40
-
41
- ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
42
- pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
43
- neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
44
- clip_loader_name = chain_definition.get('clip_loader_node', 'clip_loader')
45
- vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
46
-
47
- if ksampler_name not in assembler.node_map:
48
- print(f"Warning: Target node '{ksampler_name}' for Krea2 Style Reference Edit chain not found. Skipping.")
49
- return
50
-
51
- ksampler_id = assembler.node_map[ksampler_name]
52
-
53
- if 'model' not in assembler.workflow[ksampler_id]['inputs']:
54
- print(f"Warning: KSampler node '{ksampler_name}' is missing 'model' input. Skipping.")
55
- return
56
-
57
- current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
58
-
59
- vae_connection = None
60
- if vae_loader_name in assembler.node_map:
61
- vae_connection = [assembler.node_map[vae_loader_name], 0]
62
- else:
63
- for node_id, node in assembler.workflow.items():
64
- if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
65
- vae_connection = [node_id, 0]
66
- break
67
-
68
- clip_connection = None
69
- if clip_loader_name in assembler.node_map:
70
- clip_connection = [assembler.node_map[clip_loader_name], 0]
71
- elif pos_prompt_name in assembler.node_map:
72
- pos_id = assembler.node_map[pos_prompt_name]
73
- clip_connection = assembler.workflow[pos_id]['inputs'].get('clip')
74
-
75
- scaled_image_ids = []
76
- for i, img_filename in enumerate(valid_images):
77
- load_id = assembler._get_unique_id()
78
- load_node = create_node(assembler, "LoadImage", f"Load Reference Image {i+1}")
79
- load_node['inputs']['image'] = img_filename
80
- assembler.workflow[load_id] = load_node
81
-
82
- scale_id = assembler._get_unique_id()
83
- scale_node = create_node(assembler, "ImageScaleToTotalPixels", f"Scale Reference {i+1}")
84
- scale_node['inputs']['upscale_method'] = "nearest-exact"
85
- scale_node['inputs']['megapixels'] = 1
86
- scale_node['inputs']['resolution_steps'] = 1
87
- scale_node['inputs']['image'] = [load_id, 0]
88
- assembler.workflow[scale_id] = scale_node
89
- scaled_image_ids.append(scale_id)
90
-
91
- lora_loader_id = assembler._get_unique_id()
92
- lora_loader_node = create_node(assembler, "LoraLoaderModelOnly", "Load LoRA (Krea2 Style Reference)")
93
- lora_loader_node['inputs']['lora_name'] = lora_filename
94
- lora_loader_node['inputs']['strength_model'] = 1.0
95
- lora_loader_node['inputs']['model'] = current_model_connection
96
- assembler.workflow[lora_loader_id] = lora_loader_node
97
-
98
- assembler.workflow[ksampler_id]['inputs']['model'] = [lora_loader_id, 0]
99
-
100
- pos_prompt_id = assembler.node_map.get(pos_prompt_name)
101
- neg_prompt_id = assembler.node_map.get(neg_prompt_name)
102
-
103
- pos_text = ""
104
- if pos_prompt_id and pos_prompt_id in assembler.workflow:
105
- pos_text = assembler.workflow[pos_prompt_id]['inputs'].get('text', '')
106
- elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
107
- pos_text = assembler.ui_values.get('positive_prompt') or assembler.ui_values.get('prompt') or ''
108
-
109
- if not pos_text:
110
- for node_id, node in assembler.workflow.items():
111
- if isinstance(node, dict):
112
- cls = node.get('class_type', '')
113
- if cls in ['Krea2EditGroundedEncode', 'TextEncodeQwenImageEditPlus', 'CLIPTextEncode']:
114
- t = node.get('inputs', {}).get('prompt') or node.get('inputs', {}).get('text')
115
- if t:
116
- pos_text = t
117
- break
118
-
119
- neg_text = ""
120
- if neg_prompt_id and neg_prompt_id in assembler.workflow:
121
- neg_text = assembler.workflow[neg_prompt_id]['inputs'].get('text', '')
122
- elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
123
- neg_text = assembler.ui_values.get('negative_prompt') or assembler.ui_values.get('neg_prompt') or ''
124
-
125
- pos_encode_id = assembler._get_unique_id()
126
- pos_encode_node = create_node(assembler, "TextEncodeQwenImageEditPlus", "TextEncodeQwenImageEditPlus (Positive)")
127
- pos_encode_node['inputs']['prompt'] = pos_text
128
- if clip_connection:
129
- pos_encode_node['inputs']['clip'] = clip_connection
130
- if vae_connection:
131
- pos_encode_node['inputs']['vae'] = vae_connection
132
- for idx, s_id in enumerate(scaled_image_ids):
133
- pos_encode_node['inputs'][f"image{idx+1}"] = [s_id, 0]
134
- assembler.workflow[pos_encode_id] = pos_encode_node
135
-
136
- neg_encode_id = assembler._get_unique_id()
137
- neg_encode_node = create_node(assembler, "TextEncodeQwenImageEditPlus", "TextEncodeQwenImageEditPlus (Negative)")
138
- neg_encode_node['inputs']['prompt'] = neg_text
139
- if clip_connection:
140
- neg_encode_node['inputs']['clip'] = clip_connection
141
- if vae_connection:
142
- neg_encode_node['inputs']['vae'] = vae_connection
143
- for idx, s_id in enumerate(scaled_image_ids):
144
- neg_encode_node['inputs'][f"image{idx+1}"] = [s_id, 0]
145
- assembler.workflow[neg_encode_id] = neg_encode_node
146
-
147
- pos_ref_id = assembler._get_unique_id()
148
- pos_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
149
- pos_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
150
- pos_ref_node['inputs']['conditioning'] = [pos_encode_id, 0]
151
- assembler.workflow[pos_ref_id] = pos_ref_node
152
-
153
- neg_ref_id = assembler._get_unique_id()
154
- neg_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
155
- neg_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
156
- neg_ref_node['inputs']['conditioning'] = [neg_encode_id, 0]
157
- assembler.workflow[neg_ref_id] = neg_ref_node
158
-
159
- existing_pos = assembler.workflow[ksampler_id]['inputs'].get('positive')
160
- existing_neg = assembler.workflow[ksampler_id]['inputs'].get('negative')
161
-
162
- has_krea2_edit = False
163
- if existing_pos and isinstance(existing_pos, (list, tuple)) and len(existing_pos) > 0:
164
- pos_node_id = existing_pos[0]
165
- if pos_node_id in assembler.workflow:
166
- pos_node = assembler.workflow[pos_node_id]
167
- if isinstance(pos_node, dict) and pos_node.get('class_type') == 'Krea2EditGroundedEncode':
168
- has_krea2_edit = True
169
-
170
- if not has_krea2_edit:
171
- for node in assembler.workflow.values():
172
- if isinstance(node, dict) and node.get('class_type') in ['Krea2EditModelPatch', 'Krea2EditGroundedEncode']:
173
- has_krea2_edit = True
174
- break
175
-
176
- if has_krea2_edit and existing_pos and existing_neg:
177
- combine_pos_id = assembler._get_unique_id()
178
- combine_pos_node = create_node(assembler, "ConditioningCombine", "Conditioning (Combine)")
179
- combine_pos_node['inputs']['conditioning_1'] = existing_pos
180
- combine_pos_node['inputs']['conditioning_2'] = [pos_ref_id, 0]
181
- assembler.workflow[combine_pos_id] = combine_pos_node
182
-
183
- combine_neg_id = assembler._get_unique_id()
184
- combine_neg_node = create_node(assembler, "ConditioningCombine", "Conditioning (Combine)")
185
- combine_neg_node['inputs']['conditioning_1'] = existing_neg
186
- combine_neg_node['inputs']['conditioning_2'] = [neg_ref_id, 0]
187
- assembler.workflow[combine_neg_id] = combine_neg_node
188
-
189
- assembler.workflow[ksampler_id]['inputs']['positive'] = [combine_pos_id, 0]
190
- assembler.workflow[ksampler_id]['inputs']['negative'] = [combine_neg_id, 0]
191
- else:
192
- assembler.workflow[ksampler_id]['inputs']['positive'] = [pos_ref_id, 0]
193
- assembler.workflow[ksampler_id]['inputs']['negative'] = [neg_ref_id, 0]
194
-
195
- if pos_prompt_id and pos_prompt_id in assembler.workflow:
196
- del assembler.workflow[pos_prompt_id]
197
-
198
- if neg_prompt_id and neg_prompt_id in assembler.workflow:
199
- del assembler.workflow[neg_prompt_id]
200
-
201
- print(f"Krea2 Style Reference Edit injector applied with {len(valid_images)} reference image(s). Original CLIPTextEncode nodes replaced.")
 
1
+ import os
2
+ from utils.app_utils import ensure_file_downloaded
3
+
4
+ def create_node(assembler, class_type, title):
5
+ try:
6
+ node = assembler._get_node_template(class_type)
7
+ except Exception:
8
+ node = {
9
+ "inputs": {},
10
+ "class_type": class_type,
11
+ "_meta": {"title": title}
12
+ }
13
+ node['_meta']['title'] = title
14
+ return node
15
+
16
+ def inject(assembler, chain_definition, chain_items):
17
+ if not chain_items:
18
+ return
19
+
20
+ valid_images = []
21
+ for item in chain_items:
22
+ if not item:
23
+ continue
24
+ img_path = item
25
+ if isinstance(item, dict):
26
+ img_path = item.get('image') or item.get('filename') or item.get('path')
27
+ if img_path:
28
+ valid_images.append(img_path)
29
+
30
+ if not valid_images:
31
+ return
32
+
33
+ valid_images = valid_images[:3]
34
+
35
+ lora_filename = "krea2_style_reference.safetensors"
36
+ try:
37
+ ensure_file_downloaded(lora_filename)
38
+ except Exception as e:
39
+ print(f"Warning: Failed to ensure '{lora_filename}' downloaded: {e}")
40
+
41
+ ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
42
+ pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
43
+ neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
44
+ clip_loader_name = chain_definition.get('clip_loader_node', 'clip_loader')
45
+ vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
46
+
47
+ if ksampler_name not in assembler.node_map:
48
+ print(f"Warning: Target node '{ksampler_name}' for Krea2 Style Reference Edit chain not found. Skipping.")
49
+ return
50
+
51
+ ksampler_id = assembler.node_map[ksampler_name]
52
+
53
+ if 'model' not in assembler.workflow[ksampler_id]['inputs']:
54
+ print(f"Warning: KSampler node '{ksampler_name}' is missing 'model' input. Skipping.")
55
+ return
56
+
57
+ current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
58
+
59
+ vae_connection = None
60
+ if vae_loader_name in assembler.node_map:
61
+ vae_connection = [assembler.node_map[vae_loader_name], 0]
62
+ else:
63
+ for node_id, node in assembler.workflow.items():
64
+ if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
65
+ vae_connection = [node_id, 0]
66
+ break
67
+
68
+ clip_connection = None
69
+ if clip_loader_name in assembler.node_map:
70
+ clip_connection = [assembler.node_map[clip_loader_name], 0]
71
+ elif pos_prompt_name in assembler.node_map:
72
+ pos_id = assembler.node_map[pos_prompt_name]
73
+ clip_connection = assembler.workflow[pos_id]['inputs'].get('clip')
74
+
75
+ scaled_image_ids = []
76
+ for i, img_filename in enumerate(valid_images):
77
+ load_id = assembler._get_unique_id()
78
+ load_node = create_node(assembler, "LoadImage", f"Load Reference Image {i+1}")
79
+ load_node['inputs']['image'] = img_filename
80
+ assembler.workflow[load_id] = load_node
81
+
82
+ scale_id = assembler._get_unique_id()
83
+ scale_node = create_node(assembler, "ImageScaleToTotalPixels", f"Scale Reference {i+1}")
84
+ scale_node['inputs']['upscale_method'] = "nearest-exact"
85
+ scale_node['inputs']['megapixels'] = 1
86
+ scale_node['inputs']['resolution_steps'] = 1
87
+ scale_node['inputs']['image'] = [load_id, 0]
88
+ assembler.workflow[scale_id] = scale_node
89
+ scaled_image_ids.append(scale_id)
90
+
91
+ lora_loader_id = assembler._get_unique_id()
92
+ lora_loader_node = create_node(assembler, "LoraLoaderModelOnly", "Load LoRA (Krea2 Style Reference)")
93
+ lora_loader_node['inputs']['lora_name'] = lora_filename
94
+ lora_loader_node['inputs']['strength_model'] = 1.0
95
+ lora_loader_node['inputs']['model'] = current_model_connection
96
+ assembler.workflow[lora_loader_id] = lora_loader_node
97
+
98
+ assembler.workflow[ksampler_id]['inputs']['model'] = [lora_loader_id, 0]
99
+
100
+ pos_prompt_id = assembler.node_map.get(pos_prompt_name)
101
+ neg_prompt_id = assembler.node_map.get(neg_prompt_name)
102
+
103
+ pos_text = ""
104
+ if pos_prompt_id and pos_prompt_id in assembler.workflow:
105
+ pos_text = assembler.workflow[pos_prompt_id]['inputs'].get('text', '')
106
+ elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
107
+ pos_text = assembler.ui_values.get('positive_prompt') or assembler.ui_values.get('prompt') or ''
108
+
109
+ if not pos_text:
110
+ for node_id, node in assembler.workflow.items():
111
+ if isinstance(node, dict):
112
+ cls = node.get('class_type', '')
113
+ if cls in ['Krea2EditGroundedEncode', 'TextEncodeQwenImageEditPlus', 'CLIPTextEncode']:
114
+ t = node.get('inputs', {}).get('prompt') or node.get('inputs', {}).get('text')
115
+ if t:
116
+ pos_text = t
117
+ break
118
+
119
+ neg_text = ""
120
+ if neg_prompt_id and neg_prompt_id in assembler.workflow:
121
+ neg_text = assembler.workflow[neg_prompt_id]['inputs'].get('text', '')
122
+ elif hasattr(assembler, 'ui_values') and isinstance(assembler.ui_values, dict):
123
+ neg_text = assembler.ui_values.get('negative_prompt') or assembler.ui_values.get('neg_prompt') or ''
124
+
125
+ pos_encode_id = assembler._get_unique_id()
126
+ pos_encode_node = create_node(assembler, "TextEncodeQwenImageEditPlus", "TextEncodeQwenImageEditPlus (Positive)")
127
+ pos_encode_node['inputs']['prompt'] = pos_text
128
+ if clip_connection:
129
+ pos_encode_node['inputs']['clip'] = clip_connection
130
+ if vae_connection:
131
+ pos_encode_node['inputs']['vae'] = vae_connection
132
+ for idx, s_id in enumerate(scaled_image_ids):
133
+ pos_encode_node['inputs'][f"image{idx+1}"] = [s_id, 0]
134
+ assembler.workflow[pos_encode_id] = pos_encode_node
135
+
136
+ neg_encode_id = assembler._get_unique_id()
137
+ neg_encode_node = create_node(assembler, "TextEncodeQwenImageEditPlus", "TextEncodeQwenImageEditPlus (Negative)")
138
+ neg_encode_node['inputs']['prompt'] = neg_text
139
+ if clip_connection:
140
+ neg_encode_node['inputs']['clip'] = clip_connection
141
+ if vae_connection:
142
+ neg_encode_node['inputs']['vae'] = vae_connection
143
+ for idx, s_id in enumerate(scaled_image_ids):
144
+ neg_encode_node['inputs'][f"image{idx+1}"] = [s_id, 0]
145
+ assembler.workflow[neg_encode_id] = neg_encode_node
146
+
147
+ pos_ref_id = assembler._get_unique_id()
148
+ pos_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
149
+ pos_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
150
+ pos_ref_node['inputs']['conditioning'] = [pos_encode_id, 0]
151
+ assembler.workflow[pos_ref_id] = pos_ref_node
152
+
153
+ neg_ref_id = assembler._get_unique_id()
154
+ neg_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
155
+ neg_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
156
+ neg_ref_node['inputs']['conditioning'] = [neg_encode_id, 0]
157
+ assembler.workflow[neg_ref_id] = neg_ref_node
158
+
159
+ assembler.workflow[ksampler_id]['inputs']['positive'] = [pos_ref_id, 0]
160
+ assembler.workflow[ksampler_id]['inputs']['negative'] = [neg_ref_id, 0]
161
+
162
+ if pos_prompt_id and pos_prompt_id in assembler.workflow:
163
+ del assembler.workflow[pos_prompt_id]
164
+
165
+ if neg_prompt_id and neg_prompt_id in assembler.workflow:
166
+ del assembler.workflow[neg_prompt_id]
167
+
168
+ print(f"Krea2 Style Reference Edit injector applied with {len(valid_images)} reference image(s). Original CLIPTextEncode nodes replaced.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chain_injectors/qwen_image_edit_injector.py CHANGED
@@ -1,113 +1,113 @@
1
- def create_node(assembler, class_type, title):
2
- try:
3
- node = assembler._get_node_template(class_type)
4
- except Exception:
5
- node = {
6
- "inputs": {},
7
- "class_type": class_type,
8
- "_meta": {"title": title}
9
- }
10
- node['_meta']['title'] = title
11
- return node
12
-
13
- def inject(assembler, chain_definition, chain_items):
14
- if not chain_items:
15
- return
16
-
17
- valid_images = []
18
- for item in chain_items:
19
- if not item:
20
- continue
21
- img_path = item
22
- if isinstance(item, dict):
23
- img_path = item.get('image') or item.get('filename') or item.get('path')
24
- if img_path:
25
- valid_images.append(img_path)
26
-
27
- if not valid_images:
28
- return
29
-
30
- valid_images = valid_images[:3]
31
-
32
- ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
33
- pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
34
- neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
35
- vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
36
- model_sampler_name = chain_definition.get('model_sampler_node', 'model_sampler')
37
-
38
- if ksampler_name not in assembler.node_map:
39
- print(f"Warning: Target node '{ksampler_name}' for Qwen-Image Edit chain not found. Skipping.")
40
- return
41
-
42
- ksampler_id = assembler.node_map[ksampler_name]
43
- pos_prompt_id = assembler.node_map.get(pos_prompt_name)
44
- neg_prompt_id = assembler.node_map.get(neg_prompt_name)
45
-
46
- if not pos_prompt_id or not neg_prompt_id:
47
- print("Warning: Positive or negative prompt node not found for Qwen-Image Edit chain. Skipping.")
48
- return
49
-
50
- vae_id = assembler.node_map.get(vae_loader_name)
51
- if not vae_id:
52
- for node_id, node in assembler.workflow.items():
53
- if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
54
- vae_id = node_id
55
- break
56
-
57
- if vae_id:
58
- assembler.workflow[pos_prompt_id]['inputs']['vae'] = [vae_id, 0]
59
- assembler.workflow[neg_prompt_id]['inputs']['vae'] = [vae_id, 0]
60
-
61
- for i, img_filename in enumerate(valid_images):
62
- load_id = assembler._get_unique_id()
63
- load_node = create_node(assembler, "LoadImage", f"Load Reference Image {i+1}")
64
- load_node['inputs']['image'] = img_filename
65
- assembler.workflow[load_id] = load_node
66
-
67
- scale_id = assembler._get_unique_id()
68
- scale_node = create_node(assembler, "ImageScaleToTotalPixels", f"Scale Reference {i+1}")
69
- scale_node['inputs']['upscale_method'] = "lanczos"
70
- scale_node['inputs']['megapixels'] = 1
71
- scale_node['inputs']['resolution_steps'] = 1
72
- scale_node['inputs']['image'] = [load_id, 0]
73
- assembler.workflow[scale_id] = scale_node
74
-
75
- image_key = f"image{i+1}"
76
- assembler.workflow[pos_prompt_id]['inputs'][image_key] = [scale_id, 0]
77
- assembler.workflow[neg_prompt_id]['inputs'][image_key] = [scale_id, 0]
78
-
79
- pos_ref_id = assembler._get_unique_id()
80
- pos_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
81
- pos_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
82
- pos_ref_node['inputs']['conditioning'] = [pos_prompt_id, 0]
83
- assembler.workflow[pos_ref_id] = pos_ref_node
84
-
85
- neg_ref_id = assembler._get_unique_id()
86
- neg_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
87
- neg_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
88
- neg_ref_node['inputs']['conditioning'] = [neg_prompt_id, 0]
89
- assembler.workflow[neg_ref_id] = neg_ref_node
90
-
91
- assembler.workflow[ksampler_id]['inputs']['positive'] = [pos_ref_id, 0]
92
- assembler.workflow[ksampler_id]['inputs']['negative'] = [neg_ref_id, 0]
93
-
94
- model_sampler_id = assembler.node_map.get(model_sampler_name)
95
- if not model_sampler_id:
96
- for node_id, node in assembler.workflow.items():
97
- if isinstance(node, dict) and node.get('class_type') == 'ModelSamplingAuraFlow':
98
- model_sampler_id = node_id
99
- break
100
-
101
- if model_sampler_id and model_sampler_id in assembler.workflow:
102
- assembler.workflow[model_sampler_id]['inputs']['shift'] = 3
103
-
104
- current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
105
- cfg_norm_id = assembler._get_unique_id()
106
- cfg_norm_node = create_node(assembler, "CFGNorm", "CFGNorm")
107
- cfg_norm_node['inputs']['strength'] = 1
108
- cfg_norm_node['inputs']['pre_cfg'] = False
109
- cfg_norm_node['inputs']['model'] = current_model_connection
110
- assembler.workflow[cfg_norm_id] = cfg_norm_node
111
- assembler.workflow[ksampler_id]['inputs']['model'] = [cfg_norm_id, 0]
112
-
113
- print(f"Qwen-Image Edit injector applied with {len(valid_images)} reference image(s). Connected VAE dynamically.")
 
1
+ def create_node(assembler, class_type, title):
2
+ try:
3
+ node = assembler._get_node_template(class_type)
4
+ except Exception:
5
+ node = {
6
+ "inputs": {},
7
+ "class_type": class_type,
8
+ "_meta": {"title": title}
9
+ }
10
+ node['_meta']['title'] = title
11
+ return node
12
+
13
+ def inject(assembler, chain_definition, chain_items):
14
+ if not chain_items:
15
+ return
16
+
17
+ valid_images = []
18
+ for item in chain_items:
19
+ if not item:
20
+ continue
21
+ img_path = item
22
+ if isinstance(item, dict):
23
+ img_path = item.get('image') or item.get('filename') or item.get('path')
24
+ if img_path:
25
+ valid_images.append(img_path)
26
+
27
+ if not valid_images:
28
+ return
29
+
30
+ valid_images = valid_images[:3]
31
+
32
+ ksampler_name = chain_definition.get('ksampler_node', 'ksampler')
33
+ pos_prompt_name = chain_definition.get('pos_prompt_node', 'pos_prompt')
34
+ neg_prompt_name = chain_definition.get('neg_prompt_node', 'neg_prompt')
35
+ vae_loader_name = chain_definition.get('vae_loader_node', 'vae_loader')
36
+ model_sampler_name = chain_definition.get('model_sampler_node', 'model_sampler')
37
+
38
+ if ksampler_name not in assembler.node_map:
39
+ print(f"Warning: Target node '{ksampler_name}' for Qwen-Image Edit chain not found. Skipping.")
40
+ return
41
+
42
+ ksampler_id = assembler.node_map[ksampler_name]
43
+ pos_prompt_id = assembler.node_map.get(pos_prompt_name)
44
+ neg_prompt_id = assembler.node_map.get(neg_prompt_name)
45
+
46
+ if not pos_prompt_id or not neg_prompt_id:
47
+ print("Warning: Positive or negative prompt node not found for Qwen-Image Edit chain. Skipping.")
48
+ return
49
+
50
+ vae_id = assembler.node_map.get(vae_loader_name)
51
+ if not vae_id:
52
+ for node_id, node in assembler.workflow.items():
53
+ if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
54
+ vae_id = node_id
55
+ break
56
+
57
+ if vae_id:
58
+ assembler.workflow[pos_prompt_id]['inputs']['vae'] = [vae_id, 0]
59
+ assembler.workflow[neg_prompt_id]['inputs']['vae'] = [vae_id, 0]
60
+
61
+ for i, img_filename in enumerate(valid_images):
62
+ load_id = assembler._get_unique_id()
63
+ load_node = create_node(assembler, "LoadImage", f"Load Reference Image {i+1}")
64
+ load_node['inputs']['image'] = img_filename
65
+ assembler.workflow[load_id] = load_node
66
+
67
+ scale_id = assembler._get_unique_id()
68
+ scale_node = create_node(assembler, "ImageScaleToTotalPixels", f"Scale Reference {i+1}")
69
+ scale_node['inputs']['upscale_method'] = "lanczos"
70
+ scale_node['inputs']['megapixels'] = 1
71
+ scale_node['inputs']['resolution_steps'] = 1
72
+ scale_node['inputs']['image'] = [load_id, 0]
73
+ assembler.workflow[scale_id] = scale_node
74
+
75
+ image_key = f"image{i+1}"
76
+ assembler.workflow[pos_prompt_id]['inputs'][image_key] = [scale_id, 0]
77
+ assembler.workflow[neg_prompt_id]['inputs'][image_key] = [scale_id, 0]
78
+
79
+ pos_ref_id = assembler._get_unique_id()
80
+ pos_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
81
+ pos_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
82
+ pos_ref_node['inputs']['conditioning'] = [pos_prompt_id, 0]
83
+ assembler.workflow[pos_ref_id] = pos_ref_node
84
+
85
+ neg_ref_id = assembler._get_unique_id()
86
+ neg_ref_node = create_node(assembler, "FluxKontextMultiReferenceLatentMethod", "Edit Model Reference Method")
87
+ neg_ref_node['inputs']['reference_latents_method'] = "index_timestep_zero"
88
+ neg_ref_node['inputs']['conditioning'] = [neg_prompt_id, 0]
89
+ assembler.workflow[neg_ref_id] = neg_ref_node
90
+
91
+ assembler.workflow[ksampler_id]['inputs']['positive'] = [pos_ref_id, 0]
92
+ assembler.workflow[ksampler_id]['inputs']['negative'] = [neg_ref_id, 0]
93
+
94
+ model_sampler_id = assembler.node_map.get(model_sampler_name)
95
+ if not model_sampler_id:
96
+ for node_id, node in assembler.workflow.items():
97
+ if isinstance(node, dict) and node.get('class_type') == 'ModelSamplingAuraFlow':
98
+ model_sampler_id = node_id
99
+ break
100
+
101
+ if model_sampler_id and model_sampler_id in assembler.workflow:
102
+ assembler.workflow[model_sampler_id]['inputs']['shift'] = 3
103
+
104
+ current_model_connection = assembler.workflow[ksampler_id]['inputs']['model']
105
+ cfg_norm_id = assembler._get_unique_id()
106
+ cfg_norm_node = create_node(assembler, "CFGNorm", "CFGNorm")
107
+ cfg_norm_node['inputs']['strength'] = 1
108
+ cfg_norm_node['inputs']['pre_cfg'] = False
109
+ cfg_norm_node['inputs']['model'] = current_model_connection
110
+ assembler.workflow[cfg_norm_id] = cfg_norm_node
111
+ assembler.workflow[ksampler_id]['inputs']['model'] = [cfg_norm_id, 0]
112
+
113
+ print(f"Qwen-Image Edit injector applied with {len(valid_images)} reference image(s). Connected VAE dynamically.")
chain_injectors/reference_image_injector.py CHANGED
@@ -1,64 +1,64 @@
1
- import os
2
-
3
- def inject(assembler, chain_definition, chain_items):
4
- if not chain_items:
5
- return
6
-
7
- valid_images = []
8
- for item in chain_items:
9
- if not item:
10
- continue
11
- img_path = item
12
- if isinstance(item, dict):
13
- img_path = item.get('image') or item.get('filename') or item.get('path')
14
- if img_path:
15
- valid_images.append(img_path)
16
-
17
- if not valid_images:
18
- return
19
-
20
- text_encode_name = chain_definition.get('text_encode_node')
21
- text_encode_id = None
22
- if text_encode_name and text_encode_name in assembler.node_map:
23
- text_encode_id = assembler.node_map[text_encode_name]
24
- else:
25
- for node_id, node in assembler.workflow.items():
26
- if isinstance(node, dict) and node.get('class_type') == 'TextEncodeMageFlowEdit':
27
- text_encode_id = node_id
28
- break
29
-
30
- if not text_encode_id or text_encode_id not in assembler.workflow:
31
- print("Warning: TextEncodeMageFlowEdit node not found for Reference Image chain. Skipping.")
32
- return
33
-
34
- vae_node_name = chain_definition.get('vae_node', 'vae_loader')
35
- vae_node_id = assembler.node_map.get(vae_node_name)
36
- if not vae_node_id:
37
- for node_id, node in assembler.workflow.items():
38
- if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
39
- vae_node_id = node_id
40
- break
41
-
42
- if vae_node_id:
43
- assembler.workflow[text_encode_id]['inputs']['vae'] = [vae_node_id, 0]
44
-
45
- for i, img_filename in enumerate(valid_images):
46
- load_id = assembler._get_unique_id()
47
- load_node = assembler._get_node_template("LoadImage")
48
- load_node['inputs']['image'] = img_filename
49
- load_node['_meta']['title'] = f"Load Reference Image {i+1}"
50
- assembler.workflow[load_id] = load_node
51
-
52
- scale_id = assembler._get_unique_id()
53
- scale_node = assembler._get_node_template("ImageScaleToTotalPixels")
54
- scale_node['inputs']['megapixels'] = 1.0
55
- scale_node['inputs']['upscale_method'] = "nearest-exact"
56
- scale_node['inputs']['resolution_steps'] = 1
57
- scale_node['inputs']['image'] = [load_id, 0]
58
- scale_node['_meta']['title'] = f"Scale Reference {i+1}"
59
- assembler.workflow[scale_id] = scale_node
60
-
61
- input_key = f"images.image_{i+1}"
62
- assembler.workflow[text_encode_id]['inputs'][input_key] = [scale_id, 0]
63
-
64
- print(f"Reference Image injector applied. Injected {len(valid_images)} reference images to TextEncodeMageFlowEdit node '{text_encode_id}'.")
 
1
+ import os
2
+
3
+ def inject(assembler, chain_definition, chain_items):
4
+ if not chain_items:
5
+ return
6
+
7
+ valid_images = []
8
+ for item in chain_items:
9
+ if not item:
10
+ continue
11
+ img_path = item
12
+ if isinstance(item, dict):
13
+ img_path = item.get('image') or item.get('filename') or item.get('path')
14
+ if img_path:
15
+ valid_images.append(img_path)
16
+
17
+ if not valid_images:
18
+ return
19
+
20
+ text_encode_name = chain_definition.get('text_encode_node')
21
+ text_encode_id = None
22
+ if text_encode_name and text_encode_name in assembler.node_map:
23
+ text_encode_id = assembler.node_map[text_encode_name]
24
+ else:
25
+ for node_id, node in assembler.workflow.items():
26
+ if isinstance(node, dict) and node.get('class_type') == 'TextEncodeMageFlowEdit':
27
+ text_encode_id = node_id
28
+ break
29
+
30
+ if not text_encode_id or text_encode_id not in assembler.workflow:
31
+ print("Warning: TextEncodeMageFlowEdit node not found for Reference Image chain. Skipping.")
32
+ return
33
+
34
+ vae_node_name = chain_definition.get('vae_node', 'vae_loader')
35
+ vae_node_id = assembler.node_map.get(vae_node_name)
36
+ if not vae_node_id:
37
+ for node_id, node in assembler.workflow.items():
38
+ if isinstance(node, dict) and node.get('class_type') == 'VAELoader':
39
+ vae_node_id = node_id
40
+ break
41
+
42
+ if vae_node_id:
43
+ assembler.workflow[text_encode_id]['inputs']['vae'] = [vae_node_id, 0]
44
+
45
+ for i, img_filename in enumerate(valid_images):
46
+ load_id = assembler._get_unique_id()
47
+ load_node = assembler._get_node_template("LoadImage")
48
+ load_node['inputs']['image'] = img_filename
49
+ load_node['_meta']['title'] = f"Load Reference Image {i+1}"
50
+ assembler.workflow[load_id] = load_node
51
+
52
+ scale_id = assembler._get_unique_id()
53
+ scale_node = assembler._get_node_template("ImageScaleToTotalPixels")
54
+ scale_node['inputs']['megapixels'] = 1.0
55
+ scale_node['inputs']['upscale_method'] = "nearest-exact"
56
+ scale_node['inputs']['resolution_steps'] = 1
57
+ scale_node['inputs']['image'] = [load_id, 0]
58
+ scale_node['_meta']['title'] = f"Scale Reference {i+1}"
59
+ assembler.workflow[scale_id] = scale_node
60
+
61
+ input_key = f"images.image_{i+1}"
62
+ assembler.workflow[text_encode_id]['inputs'][input_key] = [scale_id, 0]
63
+
64
+ print(f"Reference Image injector applied. Injected {len(valid_images)} reference images to TextEncodeMageFlowEdit node '{text_encode_id}'.")
core/pipelines/workflow_executor.py CHANGED
@@ -1,134 +1,134 @@
1
- import torch
2
- from collections import defaultdict, deque
3
- from typing import Dict, Any, List
4
- from comfy_integration.nodes import NODE_CLASS_MAPPINGS
5
- from utils.app_utils import get_value_at_index
6
-
7
- class WorkflowExecutor:
8
- @staticmethod
9
- def topological_sort(workflow: Dict[str, Any]) -> List[str]:
10
- graph = defaultdict(list)
11
- in_degree = {node_id: 0 for node_id in workflow}
12
-
13
- for node_id, node_info in workflow.items():
14
- for input_value in node_info.get('inputs', {}).values():
15
- if isinstance(input_value, list) and len(input_value) == 2 and isinstance(input_value[0], str):
16
- source_node_id = input_value[0]
17
- if source_node_id in workflow:
18
- graph[source_node_id].append(node_id)
19
- in_degree[node_id] += 1
20
-
21
- queue = deque([node_id for node_id, degree in in_degree.items() if degree == 0])
22
-
23
- sorted_nodes = []
24
- while queue:
25
- current_node_id = queue.popleft()
26
- sorted_nodes.append(current_node_id)
27
-
28
- for neighbor_node_id in graph[current_node_id]:
29
- in_degree[neighbor_node_id] -= 1
30
- if in_degree[neighbor_node_id] == 0:
31
- queue.append(neighbor_node_id)
32
-
33
- if len(sorted_nodes) != len(workflow):
34
- raise RuntimeError("Workflow contains a cycle and cannot be executed.")
35
-
36
- return sorted_nodes
37
-
38
- @staticmethod
39
- def execute_workflow(workflow: Dict[str, Any], initial_objects: Dict[str, Any]):
40
- with torch.no_grad():
41
- computed_outputs = initial_objects
42
-
43
- try:
44
- sorted_node_ids = WorkflowExecutor.topological_sort(workflow)
45
-
46
- final_node_id = None
47
- for node_id in reversed(sorted_node_ids):
48
- if workflow[node_id].get('class_type') == 'SaveImage':
49
- final_node_id = node_id
50
- break
51
-
52
- if final_node_id:
53
- required_nodes = set()
54
- nodes_to_visit = [final_node_id]
55
- while nodes_to_visit:
56
- curr_id = nodes_to_visit.pop()
57
- if curr_id in required_nodes:
58
- continue
59
- required_nodes.add(curr_id)
60
- curr_info = workflow.get(curr_id, {})
61
- for input_val in curr_info.get('inputs', {}).values():
62
- if isinstance(input_val, list) and len(input_val) == 2 and isinstance(input_val[0], str):
63
- src_id = input_val[0]
64
- if src_id in workflow and src_id not in required_nodes:
65
- nodes_to_visit.append(src_id)
66
-
67
- sorted_node_ids = [nid for nid in sorted_node_ids if nid in required_nodes]
68
-
69
- print(f"--- [Workflow Executor] Execution order: {sorted_node_ids}")
70
- except RuntimeError as e:
71
- print("--- [Workflow Executor] ERROR: Failed to sort workflow. Dumping graph details. ---")
72
- for node_id, node_info in workflow.items():
73
- print(f" Node {node_id} ({node_info['class_type']}):")
74
- for input_name, input_value in node_info['inputs'].items():
75
- if isinstance(input_value, list) and len(input_value) == 2 and isinstance(input_value[0], str):
76
- print(f" - {input_name} <- [{input_value[0]}, {input_value[1]}]")
77
- raise e
78
-
79
- for node_id in sorted_node_ids:
80
- if node_id in computed_outputs:
81
- continue
82
-
83
- node_info = workflow[node_id]
84
- class_type = node_info['class_type']
85
-
86
- is_loader_with_filename = 'Loader' in class_type and any(key.endswith('_name') for key in node_info['inputs'])
87
- if node_id in initial_objects and is_loader_with_filename:
88
- continue
89
-
90
- node_class = NODE_CLASS_MAPPINGS.get(class_type)
91
- if node_class is None:
92
- raise RuntimeError(f"Could not find node class '{class_type}'. Is it imported in comfy_integration/nodes.py?")
93
-
94
- node_instance = node_class()
95
-
96
- kwargs = {}
97
- for param_name, param_value in node_info['inputs'].items():
98
- if isinstance(param_value, list) and len(param_value) == 2 and isinstance(param_value[0], str):
99
- source_node_id, output_index = param_value
100
- if source_node_id not in computed_outputs:
101
- raise RuntimeError(f"Workflow integrity error: Output of node {source_node_id} needed for {node_id} but not yet computed.")
102
-
103
- source_output_tuple = computed_outputs[source_node_id]
104
- actual_value = get_value_at_index(source_output_tuple, output_index)
105
- else:
106
- actual_value = param_value
107
-
108
- if '.' in param_name:
109
- parent_key, child_key = param_name.split('.', 1)
110
- if parent_key not in kwargs or not isinstance(kwargs[parent_key], dict):
111
- kwargs[parent_key] = {}
112
- kwargs[parent_key][child_key] = actual_value
113
- else:
114
- kwargs[param_name] = actual_value
115
-
116
- function_name = getattr(node_class, 'FUNCTION')
117
- execution_method = getattr(node_instance, function_name)
118
-
119
- result = execution_method(**kwargs)
120
- computed_outputs[node_id] = result
121
-
122
- final_node_id = None
123
- for node_id in reversed(sorted_node_ids):
124
- if workflow[node_id]['class_type'] == 'SaveImage':
125
- final_node_id = node_id
126
- break
127
-
128
- if not final_node_id:
129
- raise RuntimeError("Workflow does not contain a 'SaveImage' node as the output.")
130
-
131
- save_image_inputs = workflow[final_node_id]['inputs']
132
- image_source_node_id, image_source_index = save_image_inputs['images']
133
-
134
- return get_value_at_index(computed_outputs[image_source_node_id], image_source_index)
 
1
+ import torch
2
+ from collections import defaultdict, deque
3
+ from typing import Dict, Any, List
4
+ from comfy_integration.nodes import NODE_CLASS_MAPPINGS
5
+ from utils.app_utils import get_value_at_index
6
+
7
+ class WorkflowExecutor:
8
+ @staticmethod
9
+ def topological_sort(workflow: Dict[str, Any]) -> List[str]:
10
+ graph = defaultdict(list)
11
+ in_degree = {node_id: 0 for node_id in workflow}
12
+
13
+ for node_id, node_info in workflow.items():
14
+ for input_value in node_info.get('inputs', {}).values():
15
+ if isinstance(input_value, list) and len(input_value) == 2 and isinstance(input_value[0], str):
16
+ source_node_id = input_value[0]
17
+ if source_node_id in workflow:
18
+ graph[source_node_id].append(node_id)
19
+ in_degree[node_id] += 1
20
+
21
+ queue = deque([node_id for node_id, degree in in_degree.items() if degree == 0])
22
+
23
+ sorted_nodes = []
24
+ while queue:
25
+ current_node_id = queue.popleft()
26
+ sorted_nodes.append(current_node_id)
27
+
28
+ for neighbor_node_id in graph[current_node_id]:
29
+ in_degree[neighbor_node_id] -= 1
30
+ if in_degree[neighbor_node_id] == 0:
31
+ queue.append(neighbor_node_id)
32
+
33
+ if len(sorted_nodes) != len(workflow):
34
+ raise RuntimeError("Workflow contains a cycle and cannot be executed.")
35
+
36
+ return sorted_nodes
37
+
38
+ @staticmethod
39
+ def execute_workflow(workflow: Dict[str, Any], initial_objects: Dict[str, Any]):
40
+ with torch.no_grad():
41
+ computed_outputs = initial_objects
42
+
43
+ try:
44
+ sorted_node_ids = WorkflowExecutor.topological_sort(workflow)
45
+
46
+ final_node_id = None
47
+ for node_id in reversed(sorted_node_ids):
48
+ if workflow[node_id].get('class_type') == 'SaveImage':
49
+ final_node_id = node_id
50
+ break
51
+
52
+ if final_node_id:
53
+ required_nodes = set()
54
+ nodes_to_visit = [final_node_id]
55
+ while nodes_to_visit:
56
+ curr_id = nodes_to_visit.pop()
57
+ if curr_id in required_nodes:
58
+ continue
59
+ required_nodes.add(curr_id)
60
+ curr_info = workflow.get(curr_id, {})
61
+ for input_val in curr_info.get('inputs', {}).values():
62
+ if isinstance(input_val, list) and len(input_val) == 2 and isinstance(input_val[0], str):
63
+ src_id = input_val[0]
64
+ if src_id in workflow and src_id not in required_nodes:
65
+ nodes_to_visit.append(src_id)
66
+
67
+ sorted_node_ids = [nid for nid in sorted_node_ids if nid in required_nodes]
68
+
69
+ print(f"--- [Workflow Executor] Execution order: {sorted_node_ids}")
70
+ except RuntimeError as e:
71
+ print("--- [Workflow Executor] ERROR: Failed to sort workflow. Dumping graph details. ---")
72
+ for node_id, node_info in workflow.items():
73
+ print(f" Node {node_id} ({node_info['class_type']}):")
74
+ for input_name, input_value in node_info['inputs'].items():
75
+ if isinstance(input_value, list) and len(input_value) == 2 and isinstance(input_value[0], str):
76
+ print(f" - {input_name} <- [{input_value[0]}, {input_value[1]}]")
77
+ raise e
78
+
79
+ for node_id in sorted_node_ids:
80
+ if node_id in computed_outputs:
81
+ continue
82
+
83
+ node_info = workflow[node_id]
84
+ class_type = node_info['class_type']
85
+
86
+ is_loader_with_filename = 'Loader' in class_type and any(key.endswith('_name') for key in node_info['inputs'])
87
+ if node_id in initial_objects and is_loader_with_filename:
88
+ continue
89
+
90
+ node_class = NODE_CLASS_MAPPINGS.get(class_type)
91
+ if node_class is None:
92
+ raise RuntimeError(f"Could not find node class '{class_type}'. Is it imported in comfy_integration/nodes.py?")
93
+
94
+ node_instance = node_class()
95
+
96
+ kwargs = {}
97
+ for param_name, param_value in node_info['inputs'].items():
98
+ if isinstance(param_value, list) and len(param_value) == 2 and isinstance(param_value[0], str):
99
+ source_node_id, output_index = param_value
100
+ if source_node_id not in computed_outputs:
101
+ raise RuntimeError(f"Workflow integrity error: Output of node {source_node_id} needed for {node_id} but not yet computed.")
102
+
103
+ source_output_tuple = computed_outputs[source_node_id]
104
+ actual_value = get_value_at_index(source_output_tuple, output_index)
105
+ else:
106
+ actual_value = param_value
107
+
108
+ if '.' in param_name:
109
+ parent_key, child_key = param_name.split('.', 1)
110
+ if parent_key not in kwargs or not isinstance(kwargs[parent_key], dict):
111
+ kwargs[parent_key] = {}
112
+ kwargs[parent_key][child_key] = actual_value
113
+ else:
114
+ kwargs[param_name] = actual_value
115
+
116
+ function_name = getattr(node_class, 'FUNCTION')
117
+ execution_method = getattr(node_instance, function_name)
118
+
119
+ result = execution_method(**kwargs)
120
+ computed_outputs[node_id] = result
121
+
122
+ final_node_id = None
123
+ for node_id in reversed(sorted_node_ids):
124
+ if workflow[node_id]['class_type'] == 'SaveImage':
125
+ final_node_id = node_id
126
+ break
127
+
128
+ if not final_node_id:
129
+ raise RuntimeError("Workflow does not contain a 'SaveImage' node as the output.")
130
+
131
+ save_image_inputs = workflow[final_node_id]['inputs']
132
+ image_source_node_id, image_source_index = save_image_inputs['images']
133
+
134
+ return get_value_at_index(computed_outputs[image_source_node_id], image_source_index)
core/pipelines/workflow_recipes/_partials/_base_sampler.yaml CHANGED
@@ -1,28 +1,28 @@
1
- nodes:
2
- ksampler:
3
- class_type: KSampler
4
- title: "KSampler"
5
- params:
6
- denoise: 1.0
7
- vae_decode:
8
- class_type: VAEDecode
9
- title: "VAE Decode"
10
- save_image:
11
- class_type: SaveImage
12
- title: "Save Image"
13
- params: {}
14
-
15
- connections:
16
- - from: "ksampler:0"
17
- to: "vae_decode:samples"
18
- - from: "vae_decode:0"
19
- to: "save_image:images"
20
-
21
- ui_map:
22
- seed: "ksampler:seed"
23
- steps: "ksampler:steps"
24
- cfg: "ksampler:cfg"
25
- sampler_name: "ksampler:sampler_name"
26
- scheduler: "ksampler:scheduler"
27
- denoise: "ksampler:denoise"
28
  filename_prefix: "save_image:filename_prefix"
 
1
+ nodes:
2
+ ksampler:
3
+ class_type: KSampler
4
+ title: "KSampler"
5
+ params:
6
+ denoise: 1.0
7
+ vae_decode:
8
+ class_type: VAEDecode
9
+ title: "VAE Decode"
10
+ save_image:
11
+ class_type: SaveImage
12
+ title: "Save Image"
13
+ params: {}
14
+
15
+ connections:
16
+ - from: "ksampler:0"
17
+ to: "vae_decode:samples"
18
+ - from: "vae_decode:0"
19
+ to: "save_image:images"
20
+
21
+ ui_map:
22
+ seed: "ksampler:seed"
23
+ steps: "ksampler:steps"
24
+ cfg: "ksampler:cfg"
25
+ sampler_name: "ksampler:sampler_name"
26
+ scheduler: "ksampler:scheduler"
27
+ denoise: "ksampler:denoise"
28
  filename_prefix: "save_image:filename_prefix"
core/pipelines/workflow_recipes/_partials/conditioning/boogu-image.yaml CHANGED
@@ -1,67 +1,67 @@
1
- nodes:
2
- boogu_prompt:
3
- class_type: TextEncodeBooguEdit
4
- title: "Text Encode Boogu Edit"
5
- unet_loader:
6
- class_type: UNETLoader
7
- title: "Load Diffusion Model"
8
- params:
9
- weight_dtype: "default"
10
- clip_loader:
11
- class_type: CLIPLoader
12
- title: "Load CLIP"
13
- params:
14
- type: "boogu"
15
- device: "default"
16
- vae_loader:
17
- class_type: VAELoader
18
- title: "Load VAE"
19
-
20
- connections:
21
- - from: "unet_loader:0"
22
- to: "ksampler:model"
23
- - from: "clip_loader:0"
24
- to: "boogu_prompt:clip"
25
- - from: "boogu_prompt:0"
26
- to: "ksampler:positive"
27
- - from: "boogu_prompt:1"
28
- to: "ksampler:negative"
29
- - from: "vae_loader:0"
30
- to: "vae_decode:vae"
31
- - from: "vae_loader:0"
32
- to: "vae_encode:vae"
33
-
34
- dynamic_lora_chains:
35
- lora_chain:
36
- template: "LoraLoader"
37
- output_map:
38
- "unet_loader:0": "model"
39
- "clip_loader:0": "clip"
40
- input_map:
41
- "model": "model"
42
- "clip": "clip"
43
- end_input_map:
44
- "model": ["ksampler:model"]
45
- "clip": ["boogu_prompt:clip"]
46
-
47
- dynamic_conditioning_chains:
48
- conditioning_chain:
49
- ksampler_node: "ksampler"
50
- clip_source: "clip_loader:0"
51
-
52
- dynamic_boogu_image_edit_chains:
53
- boogu_image_edit_chain:
54
- ksampler_node: "ksampler"
55
- boogu_prompt_node: "boogu_prompt"
56
- vae_loader_node: "vae_loader"
57
-
58
- dynamic_pid_chains:
59
- pid_chain:
60
- ksampler_node: "ksampler"
61
-
62
- ui_map:
63
- positive_prompt: "boogu_prompt:prompt"
64
- negative_prompt: "boogu_prompt:negative_prompt"
65
- unet_name: "unet_loader:unet_name"
66
- clip_name: "clip_loader:clip_name"
67
- vae_name: "vae_loader:vae_name"
 
1
+ nodes:
2
+ boogu_prompt:
3
+ class_type: TextEncodeBooguEdit
4
+ title: "Text Encode Boogu Edit"
5
+ unet_loader:
6
+ class_type: UNETLoader
7
+ title: "Load Diffusion Model"
8
+ params:
9
+ weight_dtype: "default"
10
+ clip_loader:
11
+ class_type: CLIPLoader
12
+ title: "Load CLIP"
13
+ params:
14
+ type: "boogu"
15
+ device: "default"
16
+ vae_loader:
17
+ class_type: VAELoader
18
+ title: "Load VAE"
19
+
20
+ connections:
21
+ - from: "unet_loader:0"
22
+ to: "ksampler:model"
23
+ - from: "clip_loader:0"
24
+ to: "boogu_prompt:clip"
25
+ - from: "boogu_prompt:0"
26
+ to: "ksampler:positive"
27
+ - from: "boogu_prompt:1"
28
+ to: "ksampler:negative"
29
+ - from: "vae_loader:0"
30
+ to: "vae_decode:vae"
31
+ - from: "vae_loader:0"
32
+ to: "vae_encode:vae"
33
+
34
+ dynamic_lora_chains:
35
+ lora_chain:
36
+ template: "LoraLoader"
37
+ output_map:
38
+ "unet_loader:0": "model"
39
+ "clip_loader:0": "clip"
40
+ input_map:
41
+ "model": "model"
42
+ "clip": "clip"
43
+ end_input_map:
44
+ "model": ["ksampler:model"]
45
+ "clip": ["boogu_prompt:clip"]
46
+
47
+ dynamic_conditioning_chains:
48
+ conditioning_chain:
49
+ ksampler_node: "ksampler"
50
+ clip_source: "clip_loader:0"
51
+
52
+ dynamic_boogu_image_edit_chains:
53
+ boogu_image_edit_chain:
54
+ ksampler_node: "ksampler"
55
+ boogu_prompt_node: "boogu_prompt"
56
+ vae_loader_node: "vae_loader"
57
+
58
+ dynamic_pid_chains:
59
+ pid_chain:
60
+ ksampler_node: "ksampler"
61
+
62
+ ui_map:
63
+ positive_prompt: "boogu_prompt:prompt"
64
+ negative_prompt: "boogu_prompt:negative_prompt"
65
+ unet_name: "unet_loader:unet_name"
66
+ clip_name: "clip_loader:clip_name"
67
+ vae_name: "vae_loader:vae_name"
core/pipelines/workflow_recipes/_partials/conditioning/qwen-image.yaml CHANGED
@@ -1,91 +1,91 @@
1
- nodes:
2
- pos_prompt:
3
- class_type: TextEncodeQwenImageEditPlus
4
- title: "Text Encode Qwen Image Edit Plus (Positive)"
5
- neg_prompt:
6
- class_type: TextEncodeQwenImageEditPlus
7
- title: "Text Encode Qwen Image Edit Plus (Negative)"
8
- unet_loader:
9
- class_type: UNETLoader
10
- title: "Load Qwen UNET"
11
- params:
12
- weight_dtype: "default"
13
- vae_loader:
14
- class_type: VAELoader
15
- title: "Load Qwen VAE"
16
- clip_loader:
17
- class_type: CLIPLoader
18
- title: "Load Qwen CLIP"
19
- params:
20
- type: "qwen_image"
21
- device: "default"
22
- model_sampler:
23
- class_type: ModelSamplingAuraFlow
24
- title: "ModelSamplingAuraFlow"
25
- params:
26
- shift: 3.1
27
-
28
- connections:
29
- - from: "unet_loader:0"
30
- to: "model_sampler:model"
31
-
32
- - from: "model_sampler:0"
33
- to: "ksampler:model"
34
-
35
- - from: "clip_loader:0"
36
- to: "pos_prompt:clip"
37
- - from: "clip_loader:0"
38
- to: "neg_prompt:clip"
39
-
40
- - from: "vae_loader:0"
41
- to: "vae_decode:vae"
42
- - from: "vae_loader:0"
43
- to: "vae_encode:vae"
44
-
45
- - from: "pos_prompt:0"
46
- to: "ksampler:positive"
47
- - from: "neg_prompt:0"
48
- to: "ksampler:negative"
49
-
50
- dynamic_lora_chains:
51
- lora_chain:
52
- template: "LoraLoader"
53
- output_map:
54
- "unet_loader:0": "model"
55
- "clip_loader:0": "clip"
56
- input_map:
57
- "model": "model"
58
- "clip": "clip"
59
- end_input_map:
60
- "model": ["model_sampler:model"]
61
- "clip": ["pos_prompt:clip", "neg_prompt:clip"]
62
-
63
- dynamic_controlnet_chains:
64
- controlnet_chain:
65
- template: "ControlNetApplyAdvanced"
66
- ksampler_node: "ksampler"
67
- vae_source: "vae_loader:0"
68
-
69
- dynamic_conditioning_chains:
70
- conditioning_chain:
71
- ksampler_node: "ksampler"
72
- clip_source: "clip_loader:0"
73
-
74
- dynamic_qwen_image_edit_chains:
75
- qwen_image_edit_chain:
76
- ksampler_node: "ksampler"
77
- pos_prompt_node: "pos_prompt"
78
- neg_prompt_node: "neg_prompt"
79
- vae_loader_node: "vae_loader"
80
- model_sampler_node: "model_sampler"
81
-
82
- dynamic_pid_chains:
83
- pid_chain:
84
- ksampler_node: "ksampler"
85
-
86
- ui_map:
87
- positive_prompt: "pos_prompt:prompt"
88
- negative_prompt: "neg_prompt:prompt"
89
- unet_name: "unet_loader:unet_name"
90
- vae_name: "vae_loader:vae_name"
91
  clip_name: "clip_loader:clip_name"
 
1
+ nodes:
2
+ pos_prompt:
3
+ class_type: TextEncodeQwenImageEditPlus
4
+ title: "Text Encode Qwen Image Edit Plus (Positive)"
5
+ neg_prompt:
6
+ class_type: TextEncodeQwenImageEditPlus
7
+ title: "Text Encode Qwen Image Edit Plus (Negative)"
8
+ unet_loader:
9
+ class_type: UNETLoader
10
+ title: "Load Qwen UNET"
11
+ params:
12
+ weight_dtype: "default"
13
+ vae_loader:
14
+ class_type: VAELoader
15
+ title: "Load Qwen VAE"
16
+ clip_loader:
17
+ class_type: CLIPLoader
18
+ title: "Load Qwen CLIP"
19
+ params:
20
+ type: "qwen_image"
21
+ device: "default"
22
+ model_sampler:
23
+ class_type: ModelSamplingAuraFlow
24
+ title: "ModelSamplingAuraFlow"
25
+ params:
26
+ shift: 3.1
27
+
28
+ connections:
29
+ - from: "unet_loader:0"
30
+ to: "model_sampler:model"
31
+
32
+ - from: "model_sampler:0"
33
+ to: "ksampler:model"
34
+
35
+ - from: "clip_loader:0"
36
+ to: "pos_prompt:clip"
37
+ - from: "clip_loader:0"
38
+ to: "neg_prompt:clip"
39
+
40
+ - from: "vae_loader:0"
41
+ to: "vae_decode:vae"
42
+ - from: "vae_loader:0"
43
+ to: "vae_encode:vae"
44
+
45
+ - from: "pos_prompt:0"
46
+ to: "ksampler:positive"
47
+ - from: "neg_prompt:0"
48
+ to: "ksampler:negative"
49
+
50
+ dynamic_lora_chains:
51
+ lora_chain:
52
+ template: "LoraLoader"
53
+ output_map:
54
+ "unet_loader:0": "model"
55
+ "clip_loader:0": "clip"
56
+ input_map:
57
+ "model": "model"
58
+ "clip": "clip"
59
+ end_input_map:
60
+ "model": ["model_sampler:model"]
61
+ "clip": ["pos_prompt:clip", "neg_prompt:clip"]
62
+
63
+ dynamic_controlnet_chains:
64
+ controlnet_chain:
65
+ template: "ControlNetApplyAdvanced"
66
+ ksampler_node: "ksampler"
67
+ vae_source: "vae_loader:0"
68
+
69
+ dynamic_conditioning_chains:
70
+ conditioning_chain:
71
+ ksampler_node: "ksampler"
72
+ clip_source: "clip_loader:0"
73
+
74
+ dynamic_qwen_image_edit_chains:
75
+ qwen_image_edit_chain:
76
+ ksampler_node: "ksampler"
77
+ pos_prompt_node: "pos_prompt"
78
+ neg_prompt_node: "neg_prompt"
79
+ vae_loader_node: "vae_loader"
80
+ model_sampler_node: "model_sampler"
81
+
82
+ dynamic_pid_chains:
83
+ pid_chain:
84
+ ksampler_node: "ksampler"
85
+
86
+ ui_map:
87
+ positive_prompt: "pos_prompt:prompt"
88
+ negative_prompt: "neg_prompt:prompt"
89
+ unet_name: "unet_loader:unet_name"
90
+ vae_name: "vae_loader:vae_name"
91
  clip_name: "clip_loader:clip_name"
core/pipelines/workflow_recipes/unified_recipe.yaml CHANGED
@@ -1,8 +1,8 @@
1
- imports:
2
- - "_partials/_base_sampler.yaml"
3
- - "_partials/input/{{ task_type }}.yaml"
4
- - "_partials/conditioning/{{ model_type }}.yaml"
5
-
6
- connections:
7
- - from: "latent_source:0"
8
  to: "ksampler:latent_image"
 
1
+ imports:
2
+ - "_partials/_base_sampler.yaml"
3
+ - "_partials/input/{{ task_type }}.yaml"
4
+ - "_partials/conditioning/{{ model_type }}.yaml"
5
+
6
+ connections:
7
+ - from: "latent_source:0"
8
  to: "ksampler:latent_image"
core/workflow_assembler.py CHANGED
@@ -1,165 +1,165 @@
1
- import yaml
2
- import os
3
- import importlib
4
- from copy import deepcopy
5
- from comfy_integration.nodes import NODE_CLASS_MAPPINGS
6
- from chain_injectors import discover_injectors, get_registered_features
7
- from core.settings import FEATURES_CONFIG
8
-
9
- class WorkflowAssembler:
10
- def __init__(self, recipe_path, dynamic_values=None):
11
- self.base_path = os.path.dirname(recipe_path)
12
- self.dynamic_values = dynamic_values or {}
13
- self.node_counter = 0
14
- self.workflow = {}
15
- self.node_map = {}
16
-
17
- model_type = self.dynamic_values.get('model_type')
18
- self._load_injector_config(model_type=model_type)
19
-
20
- self.recipe = self._load_and_merge_recipe(os.path.basename(recipe_path), self.dynamic_values)
21
-
22
- def _load_injector_config(self, model_type=None):
23
- self.global_injectors = discover_injectors()
24
- registered_features = get_registered_features()
25
-
26
- order = []
27
- if model_type and model_type in FEATURES_CONFIG:
28
- enabled_features = FEATURES_CONFIG[model_type].get('enabled_chains', [])
29
- for feat in enabled_features:
30
- if feat in registered_features:
31
- chain_key = registered_features[feat]['chain_type']
32
- else:
33
- chain_key = f"dynamic_{feat}_chains"
34
- if chain_key in self.global_injectors and chain_key not in order:
35
- order.append(chain_key)
36
-
37
- for chain_key in self.global_injectors.keys():
38
- if chain_key not in order:
39
- order.append(chain_key)
40
-
41
- self.injector_order = order
42
-
43
- def _get_unique_id(self):
44
- self.node_counter += 1
45
- return str(self.node_counter)
46
-
47
- def _get_node_template(self, class_type):
48
- if class_type not in NODE_CLASS_MAPPINGS:
49
- raise ValueError(f"Node class '{class_type}' not found. Ensure it's correctly imported in comfy_integration/nodes.py.")
50
-
51
- node_class = NODE_CLASS_MAPPINGS[class_type]
52
- input_types = node_class.INPUT_TYPES()
53
-
54
- template = {
55
- "inputs": {},
56
- "class_type": class_type,
57
- "_meta": {"title": node_class.NODE_NAME if hasattr(node_class, 'NODE_NAME') else class_type}
58
- }
59
-
60
- all_inputs = {**input_types.get('required', {}), **input_types.get('optional', {})}
61
- for name, details in all_inputs.items():
62
- config = details[1] if len(details) > 1 and isinstance(details[1], dict) else {}
63
- template["inputs"][name] = config.get("default")
64
-
65
- return template
66
-
67
- def _load_and_merge_recipe(self, recipe_filename, dynamic_values, search_context_dir=None):
68
- search_path = search_context_dir or self.base_path
69
- recipe_path_to_use = os.path.join(search_path, recipe_filename)
70
-
71
- if not os.path.exists(recipe_path_to_use):
72
- raise FileNotFoundError(f"Recipe file not found: {recipe_path_to_use}")
73
-
74
- with open(recipe_path_to_use, 'r', encoding='utf-8') as f:
75
- content = f.read()
76
-
77
- for key, value in dynamic_values.items():
78
- if value is not None:
79
- content = content.replace(f"{{{{ {key} }}}}", str(value))
80
-
81
- main_recipe = yaml.safe_load(content)
82
-
83
- merged_recipe = {'nodes': {}, 'connections': [], 'ui_map': {}}
84
- for key in self.injector_order:
85
- if key.startswith('dynamic_'):
86
- merged_recipe[key] = {}
87
-
88
- parent_recipe_dir = os.path.dirname(recipe_path_to_use)
89
- for import_path_template in main_recipe.get('imports', []):
90
- import_path = import_path_template
91
- for key, value in dynamic_values.items():
92
- if value is not None:
93
- import_path = import_path.replace(f"{{{{ {key} }}}}", str(value))
94
-
95
- try:
96
- imported_recipe = self._load_and_merge_recipe(import_path, dynamic_values, search_context_dir=parent_recipe_dir)
97
- merged_recipe['nodes'].update(imported_recipe.get('nodes', {}))
98
- merged_recipe['connections'].extend(imported_recipe.get('connections', []))
99
- merged_recipe['ui_map'].update(imported_recipe.get('ui_map', {}))
100
- for key in self.injector_order:
101
- if key in imported_recipe and key.startswith('dynamic_'):
102
- merged_recipe[key].update(imported_recipe.get(key, {}))
103
- except FileNotFoundError:
104
- print(f"Warning: Optional recipe partial '{import_path}' not found. Skipping.")
105
-
106
- merged_recipe['nodes'].update(main_recipe.get('nodes', {}))
107
- merged_recipe['connections'].extend(main_recipe.get('connections', []))
108
- merged_recipe['ui_map'].update(main_recipe.get('ui_map', {}))
109
- for key in self.injector_order:
110
- if key in main_recipe and key.startswith('dynamic_'):
111
- merged_recipe[key].update(main_recipe.get(key, {}))
112
-
113
- return merged_recipe
114
-
115
- def assemble(self, ui_values):
116
- self.ui_values = ui_values
117
- for name, details in self.recipe['nodes'].items():
118
- class_type = details['class_type']
119
- template = self._get_node_template(class_type)
120
- node_data = deepcopy(template)
121
-
122
- unique_id = self._get_unique_id()
123
- self.node_map[name] = unique_id
124
-
125
- if 'params' in details:
126
- for param, value in details['params'].items():
127
- if param in node_data['inputs']:
128
- node_data['inputs'][param] = value
129
-
130
- self.workflow[unique_id] = node_data
131
-
132
- for ui_key, target in self.recipe.get('ui_map', {}).items():
133
- if ui_key in ui_values and ui_values[ui_key] is not None:
134
- target_list = target if isinstance(target, list) else [target]
135
- for t in target_list:
136
- target_name, target_param = t.split(':')
137
- if target_name in self.node_map:
138
- self.workflow[self.node_map[target_name]]['inputs'][target_param] = ui_values[ui_key]
139
-
140
- for conn in self.recipe.get('connections', []):
141
- from_name, from_output_idx = conn['from'].split(':')
142
- to_name, to_input_name = conn['to'].split(':')
143
-
144
- from_id = self.node_map.get(from_name)
145
- to_id = self.node_map.get(to_name)
146
-
147
- if from_id and to_id:
148
- self.workflow[to_id]['inputs'][to_input_name] = [from_id, int(from_output_idx)]
149
-
150
- print("--- [Assembler] Applying dynamic injectors ---")
151
- recipe_chain_types = {key for key in self.recipe if key.startswith('dynamic_')}
152
- processing_order = [key for key in self.injector_order if key in recipe_chain_types]
153
-
154
- for chain_type in processing_order:
155
- injector_func = self.global_injectors.get(chain_type)
156
- if injector_func:
157
- for chain_key, chain_def in self.recipe.get(chain_type, {}).items():
158
- if chain_key in ui_values and ui_values[chain_key]:
159
- print(f" -> Injecting '{chain_type}' for '{chain_key}'...")
160
- chain_items = ui_values[chain_key]
161
- injector_func(self, chain_def, chain_items)
162
-
163
- print("--- [Assembler] Finished applying injectors ---")
164
-
165
  return self.workflow
 
1
+ import yaml
2
+ import os
3
+ import importlib
4
+ from copy import deepcopy
5
+ from comfy_integration.nodes import NODE_CLASS_MAPPINGS
6
+ from chain_injectors import discover_injectors, get_registered_features
7
+ from core.settings import FEATURES_CONFIG
8
+
9
+ class WorkflowAssembler:
10
+ def __init__(self, recipe_path, dynamic_values=None):
11
+ self.base_path = os.path.dirname(recipe_path)
12
+ self.dynamic_values = dynamic_values or {}
13
+ self.node_counter = 0
14
+ self.workflow = {}
15
+ self.node_map = {}
16
+
17
+ model_type = self.dynamic_values.get('model_type')
18
+ self._load_injector_config(model_type=model_type)
19
+
20
+ self.recipe = self._load_and_merge_recipe(os.path.basename(recipe_path), self.dynamic_values)
21
+
22
+ def _load_injector_config(self, model_type=None):
23
+ self.global_injectors = discover_injectors()
24
+ registered_features = get_registered_features()
25
+
26
+ order = []
27
+ if model_type and model_type in FEATURES_CONFIG:
28
+ enabled_features = FEATURES_CONFIG[model_type].get('enabled_chains', [])
29
+ for feat in enabled_features:
30
+ if feat in registered_features:
31
+ chain_key = registered_features[feat]['chain_type']
32
+ else:
33
+ chain_key = f"dynamic_{feat}_chains"
34
+ if chain_key in self.global_injectors and chain_key not in order:
35
+ order.append(chain_key)
36
+
37
+ for chain_key in self.global_injectors.keys():
38
+ if chain_key not in order:
39
+ order.append(chain_key)
40
+
41
+ self.injector_order = order
42
+
43
+ def _get_unique_id(self):
44
+ self.node_counter += 1
45
+ return str(self.node_counter)
46
+
47
+ def _get_node_template(self, class_type):
48
+ if class_type not in NODE_CLASS_MAPPINGS:
49
+ raise ValueError(f"Node class '{class_type}' not found. Ensure it's correctly imported in comfy_integration/nodes.py.")
50
+
51
+ node_class = NODE_CLASS_MAPPINGS[class_type]
52
+ input_types = node_class.INPUT_TYPES()
53
+
54
+ template = {
55
+ "inputs": {},
56
+ "class_type": class_type,
57
+ "_meta": {"title": node_class.NODE_NAME if hasattr(node_class, 'NODE_NAME') else class_type}
58
+ }
59
+
60
+ all_inputs = {**input_types.get('required', {}), **input_types.get('optional', {})}
61
+ for name, details in all_inputs.items():
62
+ config = details[1] if len(details) > 1 and isinstance(details[1], dict) else {}
63
+ template["inputs"][name] = config.get("default")
64
+
65
+ return template
66
+
67
+ def _load_and_merge_recipe(self, recipe_filename, dynamic_values, search_context_dir=None):
68
+ search_path = search_context_dir or self.base_path
69
+ recipe_path_to_use = os.path.join(search_path, recipe_filename)
70
+
71
+ if not os.path.exists(recipe_path_to_use):
72
+ raise FileNotFoundError(f"Recipe file not found: {recipe_path_to_use}")
73
+
74
+ with open(recipe_path_to_use, 'r', encoding='utf-8') as f:
75
+ content = f.read()
76
+
77
+ for key, value in dynamic_values.items():
78
+ if value is not None:
79
+ content = content.replace(f"{{{{ {key} }}}}", str(value))
80
+
81
+ main_recipe = yaml.safe_load(content)
82
+
83
+ merged_recipe = {'nodes': {}, 'connections': [], 'ui_map': {}}
84
+ for key in self.injector_order:
85
+ if key.startswith('dynamic_'):
86
+ merged_recipe[key] = {}
87
+
88
+ parent_recipe_dir = os.path.dirname(recipe_path_to_use)
89
+ for import_path_template in main_recipe.get('imports', []):
90
+ import_path = import_path_template
91
+ for key, value in dynamic_values.items():
92
+ if value is not None:
93
+ import_path = import_path.replace(f"{{{{ {key} }}}}", str(value))
94
+
95
+ try:
96
+ imported_recipe = self._load_and_merge_recipe(import_path, dynamic_values, search_context_dir=parent_recipe_dir)
97
+ merged_recipe['nodes'].update(imported_recipe.get('nodes', {}))
98
+ merged_recipe['connections'].extend(imported_recipe.get('connections', []))
99
+ merged_recipe['ui_map'].update(imported_recipe.get('ui_map', {}))
100
+ for key in self.injector_order:
101
+ if key in imported_recipe and key.startswith('dynamic_'):
102
+ merged_recipe[key].update(imported_recipe.get(key, {}))
103
+ except FileNotFoundError:
104
+ print(f"Warning: Optional recipe partial '{import_path}' not found. Skipping.")
105
+
106
+ merged_recipe['nodes'].update(main_recipe.get('nodes', {}))
107
+ merged_recipe['connections'].extend(main_recipe.get('connections', []))
108
+ merged_recipe['ui_map'].update(main_recipe.get('ui_map', {}))
109
+ for key in self.injector_order:
110
+ if key in main_recipe and key.startswith('dynamic_'):
111
+ merged_recipe[key].update(main_recipe.get(key, {}))
112
+
113
+ return merged_recipe
114
+
115
+ def assemble(self, ui_values):
116
+ self.ui_values = ui_values
117
+ for name, details in self.recipe['nodes'].items():
118
+ class_type = details['class_type']
119
+ template = self._get_node_template(class_type)
120
+ node_data = deepcopy(template)
121
+
122
+ unique_id = self._get_unique_id()
123
+ self.node_map[name] = unique_id
124
+
125
+ if 'params' in details:
126
+ for param, value in details['params'].items():
127
+ if param in node_data['inputs']:
128
+ node_data['inputs'][param] = value
129
+
130
+ self.workflow[unique_id] = node_data
131
+
132
+ for ui_key, target in self.recipe.get('ui_map', {}).items():
133
+ if ui_key in ui_values and ui_values[ui_key] is not None:
134
+ target_list = target if isinstance(target, list) else [target]
135
+ for t in target_list:
136
+ target_name, target_param = t.split(':')
137
+ if target_name in self.node_map:
138
+ self.workflow[self.node_map[target_name]]['inputs'][target_param] = ui_values[ui_key]
139
+
140
+ for conn in self.recipe.get('connections', []):
141
+ from_name, from_output_idx = conn['from'].split(':')
142
+ to_name, to_input_name = conn['to'].split(':')
143
+
144
+ from_id = self.node_map.get(from_name)
145
+ to_id = self.node_map.get(to_name)
146
+
147
+ if from_id and to_id:
148
+ self.workflow[to_id]['inputs'][to_input_name] = [from_id, int(from_output_idx)]
149
+
150
+ print("--- [Assembler] Applying dynamic injectors ---")
151
+ recipe_chain_types = {key for key in self.recipe if key.startswith('dynamic_')}
152
+ processing_order = [key for key in self.injector_order if key in recipe_chain_types]
153
+
154
+ for chain_type in processing_order:
155
+ injector_func = self.global_injectors.get(chain_type)
156
+ if injector_func:
157
+ for chain_key, chain_def in self.recipe.get(chain_type, {}).items():
158
+ if chain_key in ui_values and ui_values[chain_key]:
159
+ print(f" -> Injecting '{chain_type}' for '{chain_key}'...")
160
+ chain_items = ui_values[chain_key]
161
+ injector_func(self, chain_def, chain_items)
162
+
163
+ print("--- [Assembler] Finished applying injectors ---")
164
+
165
  return self.workflow
mcp_tools/__init__.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def __getattr__(name):
2
+ if name in ("types", "server", "client", "shared"):
3
+ raise ImportError(f"No module named 'mcp.{name}' in local mcp package")
4
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
5
+
6
+ from .get_task_list import handle_get_task_list
7
+ from .get_model_architecture_list import handle_get_model_architecture_list
8
+ from .get_model_list import handle_get_model_list
9
+ from .get_feature_list import handle_get_feature_list
10
+ from .get_model_features import handle_get_model_features
11
+ from .get_chain_schema import handle_get_chain_schema
12
+ from .run_imagegen import handle_run_imagegen
13
+ from .get_task_status import handle_get_task_status
14
+ from .error_schema import make_error, make_validation_error, make_not_found_error
15
+ from .mcp_gradio_integration import (
16
+ register_high_level_mcp_apis,
17
+ cleanup_dependencies_api_names,
18
+ patch_gradio_api_suppression,
19
+ HIGH_LEVEL_MCP_API_NAMES,
20
+ )
21
+
22
+ MCP_FUNCTIONS = [
23
+ handle_get_task_list,
24
+ handle_get_model_architecture_list,
25
+ handle_get_model_list,
26
+ handle_get_feature_list,
27
+ handle_get_model_features,
28
+ handle_run_imagegen,
29
+ handle_get_task_status,
30
+ handle_get_chain_schema,
31
+ ]
32
+
33
+ __all__ = [
34
+ "handle_get_task_list",
35
+ "handle_get_model_architecture_list",
36
+ "handle_get_model_list",
37
+ "handle_get_feature_list",
38
+ "handle_get_model_features",
39
+ "handle_get_chain_schema",
40
+ "handle_run_imagegen",
41
+ "handle_get_task_status",
42
+ "make_error",
43
+ "make_validation_error",
44
+ "make_not_found_error",
45
+ "register_high_level_mcp_apis",
46
+ "cleanup_dependencies_api_names",
47
+ "patch_gradio_api_suppression",
48
+ "HIGH_LEVEL_MCP_API_NAMES",
49
+ "MCP_FUNCTIONS",
50
+ ]
mcp_tools/common.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Common Utilities & Data Structures
3
+ Contains YAML loading utilities, config file paths, task definitions, and async task database.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import urllib.parse
9
+ import urllib.request
10
+ import base64
11
+ import io
12
+ import yaml
13
+ from typing import Dict, Any
14
+ from PIL import Image
15
+
16
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ _YAML_DIR = os.path.join(_PROJECT_ROOT, "yaml")
18
+
19
+ _MODEL_ARCHITECTURES_PATH = os.path.join(_YAML_DIR, "model_architectures.yaml")
20
+ _MODEL_LIST_PATH = os.path.join(_YAML_DIR, "model_list.yaml")
21
+ _MODEL_DEFAULTS_PATH = os.path.join(_YAML_DIR, "model_defaults.yaml")
22
+ _IMAGE_GEN_FEATURES_PATH = os.path.join(_YAML_DIR, "image_gen_features.yaml")
23
+ _CHAIN_FEATURES_PATH = os.path.join(_YAML_DIR, "chain_features.yaml")
24
+ _CONSTANTS_PATH = os.path.join(_YAML_DIR, "constants.yaml")
25
+
26
+
27
+ def _parse_image_param(image_param: Any) -> Any:
28
+ """Parse a Base64 Data URI, local file path, or PIL.Image into a PIL Image object. HTTP URLs are not supported."""
29
+ if isinstance(image_param, Image.Image):
30
+ return image_param
31
+
32
+ if not isinstance(image_param, str) or not image_param.strip():
33
+ return None
34
+
35
+ image_param = image_param.strip()
36
+
37
+ # Reject HTTP / HTTPS URL
38
+ if image_param.startswith("http://") or image_param.startswith("https://"):
39
+ raise ValueError(
40
+ "Image URLs are not supported. Please supply the image directly as a Base64 Data URI (e.g., 'data:image/png;base64,...')."
41
+ )
42
+
43
+ # Base64 Data URI (e.g. data:image/png;base64,...)
44
+ if image_param.startswith("data:image/"):
45
+ _, encoded = image_param.split(",", 1) if "," in image_param else ("", image_param)
46
+ data = base64.b64decode(encoded)
47
+ return Image.open(io.BytesIO(data))
48
+
49
+ # Base64 string without header
50
+ if len(image_param) > 100 and not os.path.exists(image_param):
51
+ try:
52
+ data = base64.b64decode(image_param)
53
+ return Image.open(io.BytesIO(data))
54
+ except Exception:
55
+ pass
56
+
57
+ # Local file path
58
+ if os.path.exists(image_param):
59
+ return Image.open(image_param)
60
+
61
+ raise ValueError(
62
+ "Invalid image parameter format. Expected a Base64 Data URI (e.g., 'data:image/png;base64,...') or local file path."
63
+ )
64
+
65
+
66
+ def _load_yaml(filepath: str) -> dict:
67
+ """Safely load a YAML file, returning an empty dict if the file does not exist."""
68
+ if not os.path.exists(filepath):
69
+ print(f"Warning: YAML file not found: {filepath}")
70
+ return {}
71
+ with open(filepath, "r", encoding="utf-8") as f:
72
+ return yaml.safe_load(f) or {}
73
+
74
+
75
+ _COMMON_OPTIONAL_INPUTS = [
76
+ "steps", "cfg", "sampler", "scheduler", "seed",
77
+ "negative_prompt", "batch_size", "chain", "async_execution",
78
+ ]
79
+
80
+ _TASK_DEFINITIONS = [
81
+ {
82
+ "task_type": "txt2img",
83
+ "display_name": "Text-to-Image",
84
+ "description": "Generate images from text prompts. Canvas width and height must be specified.",
85
+ "required_inputs": ["prompt", "width", "height"],
86
+ "optional_inputs": _COMMON_OPTIONAL_INPUTS,
87
+ },
88
+ {
89
+ "task_type": "img2img",
90
+ "display_name": "Image-to-Image",
91
+ "description": "Perform global repaint and style transfer based on a source image. Denoise strength must be specified.",
92
+ "required_inputs": ["prompt", "image", "denoise"],
93
+ "optional_inputs": _COMMON_OPTIONAL_INPUTS,
94
+ },
95
+ {
96
+ "task_type": "inpaint",
97
+ "display_name": "Inpaint",
98
+ "description": "Repaint specified masked regions of the input image (with alpha mask/channel).",
99
+ "required_inputs": ["prompt", "image"],
100
+ "optional_inputs": ["denoise"] + _COMMON_OPTIONAL_INPUTS,
101
+ },
102
+ {
103
+ "task_type": "outpaint",
104
+ "display_name": "Outpaint",
105
+ "description": "Extend the canvas outward from the source image. Padding pixel values for top, bottom, left, and right must be specified.",
106
+ "required_inputs": ["prompt", "image", "pad_left", "pad_right", "pad_top", "pad_bottom"],
107
+ "optional_inputs": _COMMON_OPTIONAL_INPUTS,
108
+ },
109
+ {
110
+ "task_type": "hires_fix",
111
+ "display_name": "Hi-Res Fix / Upscale",
112
+ "description": "Enhance details and upscale an existing low-resolution image.",
113
+ "required_inputs": ["prompt", "image", "upscale_by"],
114
+ "optional_inputs": _COMMON_OPTIONAL_INPUTS,
115
+ },
116
+ ]
117
+
118
+ _TASKS_DB: Dict[str, Dict[str, Any]] = {}
119
+
120
+
121
+ class DummyProgress:
122
+ def __call__(self, progress=0.0, desc=None):
123
+ pass
124
+
125
+
126
+ def _get_public_base_url() -> str:
127
+ """Auto-resolve the publicly accessible base URL (including protocol and port)."""
128
+ # 1. Explicit environment variable override
129
+ public_url = os.getenv("PUBLIC_URL") or os.getenv("BASE_URL")
130
+ if public_url:
131
+ return public_url.rstrip("/")
132
+
133
+ # 2. Hugging Face Space environment variable
134
+ space_host = os.getenv("SPACE_HOST")
135
+ if space_host:
136
+ if not space_host.startswith("http://") and not space_host.startswith("https://"):
137
+ return f"https://{space_host}"
138
+ return space_host.rstrip("/")
139
+
140
+ # 3. Local Gradio config fallback
141
+ try:
142
+ from core.settings import GRADIO_SERVER_NAME, SERVER_PORT
143
+ except ImportError:
144
+ GRADIO_SERVER_NAME = "127.0.0.1"
145
+ SERVER_PORT = 7860
146
+
147
+ server_name = os.getenv("GRADIO_SERVER_NAME", GRADIO_SERVER_NAME)
148
+ if server_name == "0.0.0.0":
149
+ server_name = "127.0.0.1"
150
+ port = os.getenv("GRADIO_SERVER_PORT", str(SERVER_PORT))
151
+
152
+ return f"http://{server_name}:{port}"
153
+
154
+
155
+ def _execute_imagegen_pipeline(task_id: str, params: dict):
156
+ """Execute the image generation pipeline in the background and update _TASKS_DB."""
157
+ start_time = time.time()
158
+ try:
159
+ _TASKS_DB[task_id]["status"] = "processing"
160
+ _TASKS_DB[task_id]["progress"] = 10
161
+ _TASKS_DB[task_id]["updated_at"] = int(start_time)
162
+
163
+ from core.generation_logic import sd_image_pipeline
164
+
165
+ task_type = params["task_type"]
166
+ model = params["model"]
167
+ prompt = params["prompt"]
168
+
169
+ model_defaults = _load_yaml(_MODEL_DEFAULTS_PATH)
170
+ model_list = _load_yaml(_MODEL_LIST_PATH)
171
+ checkpoints = model_list.get("Checkpoint", {})
172
+ found_arch = None
173
+ for arch_name, arch_data in checkpoints.items():
174
+ if isinstance(arch_data, dict):
175
+ for m in arch_data.get("models", []):
176
+ if m.get("display_name") == model:
177
+ found_arch = arch_name
178
+ break
179
+ if found_arch:
180
+ break
181
+
182
+ arch_defaults_section = model_defaults.get(found_arch, {}) if found_arch else {}
183
+ arch_level_defaults = arch_defaults_section.get("_defaults", {})
184
+ model_specific_defaults = arch_defaults_section.get(model, {})
185
+ global_defaults = model_defaults.get("Default", {})
186
+ merged_defaults = {**global_defaults, **arch_level_defaults, **model_specific_defaults}
187
+
188
+ steps = params.get("steps") if params.get("steps") is not None else merged_defaults.get("steps", 20)
189
+ cfg = params.get("cfg") if params.get("cfg") is not None else merged_defaults.get("cfg", 1.0)
190
+ sampler = params.get("sampler") or merged_defaults.get("sampler_name", "euler")
191
+ scheduler = params.get("scheduler") or merged_defaults.get("scheduler", "simple")
192
+
193
+ ui_inputs = {
194
+ "task_type": task_type,
195
+ "model_display_name": model,
196
+ "base_model_" + task_type: model,
197
+ "positive_prompt": prompt,
198
+ "negative_prompt": params.get("negative_prompt", merged_defaults.get("negative_prompt", "")),
199
+ "width": params.get("width", 1024),
200
+ "height": params.get("height", 1024),
201
+ "num_inference_steps": steps,
202
+ "guidance_scale": cfg,
203
+ "sampler": sampler,
204
+ "scheduler": scheduler,
205
+ "seed": params.get("seed", -1),
206
+ "batch_size": params.get("batch_size", 1),
207
+ "zero_gpu_duration": params.get("zero_gpu_duration"),
208
+ "denoise": params.get("denoise", 1.0),
209
+ }
210
+
211
+ if "image" in params and params["image"]:
212
+ pil_img = _parse_image_param(params["image"])
213
+ if pil_img:
214
+ if task_type == "img2img":
215
+ ui_inputs["img2img_image"] = pil_img
216
+ ui_inputs["img2img_denoise"] = params.get("denoise", 0.7)
217
+ elif task_type == "inpaint":
218
+ ui_inputs["inpaint_image"] = pil_img
219
+ ui_inputs["inpaint_denoise"] = params.get("denoise", 1.0)
220
+ elif task_type == "outpaint":
221
+ ui_inputs["outpaint_image"] = pil_img
222
+ ui_inputs["left"] = params.get("pad_left", 0)
223
+ ui_inputs["right"] = params.get("pad_right", 0)
224
+ ui_inputs["top"] = params.get("pad_top", 0)
225
+ ui_inputs["bottom"] = params.get("pad_bottom", 0)
226
+ ui_inputs["feathering"] = params.get("feathering", 10)
227
+ elif task_type == "hires_fix":
228
+ ui_inputs["hires_image"] = pil_img
229
+ ui_inputs["hires_upscaler"] = params.get("upscaler", "latent")
230
+ ui_inputs["hires_scale_by"] = params.get("upscale_by", 2.0)
231
+ ui_inputs["hires_denoise"] = params.get("denoise", 0.55)
232
+
233
+ chain = params.get("chain", [])
234
+ if chain:
235
+ lora_data = []
236
+ controlnet_data = []
237
+ ipadapter_data = []
238
+ style_data = []
239
+
240
+ for item in chain:
241
+ itype = item.get("injector_type")
242
+ if itype == "lora":
243
+ lora_data.extend([
244
+ item.get("lora_source", "Civitai"),
245
+ item.get("lora_value", ""),
246
+ item.get("scale", 1.0),
247
+ None
248
+ ])
249
+ elif itype in ("controlnet", "krea2_controlnet", "anima_controlnet_lllite"):
250
+ controlnet_data.extend([
251
+ item.get("control_net_name", ""),
252
+ _parse_image_param(item.get("image")),
253
+ item.get("strength", 1.0)
254
+ ])
255
+ elif itype in ("ipadapter", "flux1_ipadapter", "sd3_ipadapter"):
256
+ ipadapter_data.extend([
257
+ item.get("preset", "STANDARD (medium strength)"),
258
+ _parse_image_param(item.get("image")),
259
+ item.get("weight", 1.0)
260
+ ])
261
+ elif itype == "style":
262
+ style_data.extend([
263
+ _parse_image_param(item.get("image")),
264
+ item.get("strength", 1.0)
265
+ ])
266
+
267
+ if lora_data: ui_inputs["lora_data"] = lora_data
268
+ if controlnet_data: ui_inputs["controlnet_data"] = controlnet_data
269
+ if ipadapter_data: ui_inputs["ipadapter_data"] = ipadapter_data
270
+ if style_data: ui_inputs["style_data"] = style_data
271
+
272
+ _TASKS_DB[task_id]["progress"] = 50
273
+
274
+ # Execute Pipeline
275
+ output = sd_image_pipeline.run(ui_inputs=ui_inputs, progress=DummyProgress())
276
+
277
+ try:
278
+ from core.settings import OUTPUT_DIR
279
+ except ImportError:
280
+ OUTPUT_DIR = os.path.join(_PROJECT_ROOT, "output")
281
+
282
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
283
+
284
+ import tempfile
285
+ import gradio.processing_utils as pu
286
+
287
+ gradio_cache_dir = os.path.join(tempfile.gettempdir(), "gradio")
288
+ os.makedirs(gradio_cache_dir, exist_ok=True)
289
+
290
+ base_url = _get_public_base_url()
291
+ images = []
292
+ raw_list = output if isinstance(output, list) else ([output] if output else [])
293
+ for idx, item in enumerate(raw_list):
294
+ target_path = None
295
+ if hasattr(item, "save"): # PIL Image
296
+ filename = f"mcp_{task_id}_{idx}.png"
297
+ filepath = os.path.join(OUTPUT_DIR, filename)
298
+ item.save(filepath)
299
+ target_path = filepath
300
+ elif isinstance(item, str) and os.path.exists(item):
301
+ target_path = item
302
+
303
+ if target_path:
304
+ try:
305
+ cached_path = pu.save_file_to_cache(target_path, cache_dir=gradio_cache_dir)
306
+ abs_path = os.path.abspath(cached_path).replace("\\", "/")
307
+ except Exception as e:
308
+ print(f"Warning: Failed to cache image file to Gradio temp dir: {e}")
309
+ abs_path = os.path.abspath(target_path).replace("\\", "/")
310
+
311
+ url = f"{base_url}/gradio_api/file={urllib.parse.quote(abs_path)}"
312
+ images.append(url)
313
+ elif item:
314
+ images.append(str(item))
315
+
316
+ execution_time = round(time.time() - start_time, 2)
317
+ _TASKS_DB[task_id]["status"] = "completed"
318
+ _TASKS_DB[task_id]["progress"] = 100
319
+ _TASKS_DB[task_id]["completed_at"] = int(time.time())
320
+ _TASKS_DB[task_id]["result"] = {
321
+ "images": images,
322
+ "seed": params.get("seed", -1),
323
+ "width": params.get("width", 1024),
324
+ "height": params.get("height", 1024),
325
+ "execution_time_seconds": execution_time,
326
+ }
327
+
328
+ except Exception as e:
329
+ _TASKS_DB[task_id]["status"] = "failed"
330
+ _TASKS_DB[task_id]["progress"] = 0
331
+ _TASKS_DB[task_id]["failed_at"] = int(time.time())
332
+ _TASKS_DB[task_id]["error"] = {
333
+ "code": "EXECUTION_ERROR",
334
+ "message": str(e),
335
+ }
mcp_tools/error_schema.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unified MCP tool error response format.
3
+
4
+ Error code enumeration:
5
+ - INVALID_PARAMS: Parameter validation failed (missing required fields, type errors, value out of range)
6
+ - MODEL_NOT_FOUND: The specified model name does not exist
7
+ - ARCHITECTURE_NOT_FOUND: The specified architecture name does not exist
8
+ - CHAIN_TYPE_NOT_FOUND: The specified chain/injector type is invalid
9
+ - FEATURE_NOT_SUPPORTED: The current model does not support the requested feature
10
+ - TASK_NOT_FOUND: The async task ID does not exist
11
+ - MODEL_OOM: GPU out of memory
12
+ - INTERNAL_ERROR: Internal server error
13
+ """
14
+
15
+
16
+ def make_error(code: str, message: str, details: dict = None) -> dict:
17
+ """
18
+ Construct a unified MCP tool error response.
19
+
20
+ Args:
21
+ code: Error code (UPPER_SNAKE_CASE format)
22
+ message: Human-readable error description
23
+ details: Optional details dictionary
24
+
25
+ Returns:
26
+ Standardized error response dictionary
27
+ """
28
+ error = {
29
+ "error": {
30
+ "code": code,
31
+ "message": message,
32
+ }
33
+ }
34
+ if details:
35
+ error["error"]["details"] = details
36
+ return error
37
+
38
+
39
+ def make_validation_error(
40
+ message: str = "Request validation failed.",
41
+ missing_fields: list = None,
42
+ invalid_fields: dict = None,
43
+ ) -> dict:
44
+ """
45
+ Construct a parameter validation failure error response.
46
+
47
+ Args:
48
+ message: Error description
49
+ missing_fields: List of missing required field names
50
+ invalid_fields: Key-value pairs of invalid fields, key=field name, value=reason description
51
+
52
+ Returns:
53
+ Standardized INVALID_PARAMS error response
54
+ """
55
+ details = {}
56
+ if missing_fields:
57
+ details["missing_fields"] = missing_fields
58
+ if invalid_fields:
59
+ details["invalid_fields"] = invalid_fields
60
+ return make_error("INVALID_PARAMS", message, details if details else None)
61
+
62
+
63
+ def make_not_found_error(resource_type: str, resource_id: str) -> dict:
64
+ """
65
+ Construct a resource-not-found error response.
66
+
67
+ Args:
68
+ resource_type: Resource type (e.g., "model", "architecture", "chain_type", "task")
69
+ resource_id: Resource identifier
70
+
71
+ Returns:
72
+ Standardized *_NOT_FOUND error response
73
+ """
74
+ code_map = {
75
+ "model": "MODEL_NOT_FOUND",
76
+ "architecture": "ARCHITECTURE_NOT_FOUND",
77
+ "chain_type": "CHAIN_TYPE_NOT_FOUND",
78
+ "task": "TASK_NOT_FOUND",
79
+ }
80
+ code = code_map.get(resource_type, f"{resource_type.upper()}_NOT_FOUND")
81
+ return make_error(
82
+ code,
83
+ f"The specified {resource_type} '{resource_id}' was not found.",
84
+ {"resource_type": resource_type, "resource_id": resource_id},
85
+ )
mcp_tools/get_chain_schema.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_chain_schema
3
+ Get the complete parameter schema and usage guide for a specified chain/injector type.
4
+ """
5
+
6
+ from .common import _load_yaml, _CHAIN_FEATURES_PATH
7
+ from .error_schema import make_validation_error, make_not_found_error
8
+
9
+
10
+ def handle_get_chain_schema(chain_type: str) -> dict:
11
+ """Get the complete parameter schema and usage guide for a specified chain/injector type."""
12
+ if not chain_type:
13
+ return make_validation_error(
14
+ "Parameter 'chain_type' is required.",
15
+ missing_fields=["chain_type"],
16
+ )
17
+
18
+ chain_features = _load_yaml(_CHAIN_FEATURES_PATH)
19
+
20
+ if chain_type not in chain_features:
21
+ return make_not_found_error("chain_type", chain_type)
22
+
23
+ chain_data = chain_features[chain_type]
24
+ return {
25
+ "feature_name": chain_type,
26
+ "display_name": chain_data.get("display_name", chain_type),
27
+ "description": chain_data.get("description", ""),
28
+ "supported_tasks": chain_data.get("supported_tasks", []),
29
+ "max_count": chain_data.get("max_count", 1),
30
+ "usage_guideline": chain_data.get("usage_guideline", ""),
31
+ "parameters_schema": chain_data.get("parameters_schema", {}),
32
+ }
mcp_tools/get_feature_list.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_feature_list
3
+ Get the list of supported advanced features along with their usage constraints and parameter schemas.
4
+ """
5
+
6
+ from .common import _load_yaml, _CHAIN_FEATURES_PATH
7
+
8
+
9
+ def handle_get_feature_list() -> list:
10
+ """Dynamically load the list of supported advanced features from chain_features.yaml."""
11
+ chain_features = _load_yaml(_CHAIN_FEATURES_PATH)
12
+ result = []
13
+
14
+ for chain_name, chain_data in chain_features.items():
15
+ if chain_data.get("visibility", "public") != "public":
16
+ continue
17
+
18
+ entry = {
19
+ "feature_name": chain_name,
20
+ "display_name": chain_data.get("display_name", chain_name),
21
+ "description": chain_data.get("description", ""),
22
+ "supported_tasks": chain_data.get("supported_tasks", []),
23
+ "max_count": chain_data.get("max_count", 1),
24
+ "usage_guideline": chain_data.get("usage_guideline", ""),
25
+ "parameters_schema": chain_data.get("parameters_schema", {}),
26
+ }
27
+ result.append(entry)
28
+
29
+ return result
mcp_tools/get_model_architecture_list.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_model_architecture_list
3
+ Get all supported model architectures and their corresponding default resolutions.
4
+ """
5
+
6
+ from .common import _load_yaml, _MODEL_ARCHITECTURES_PATH, _CONSTANTS_PATH
7
+
8
+
9
+ def handle_get_model_architecture_list() -> list:
10
+ """Dynamically load all supported model architectures from model_architectures.yaml."""
11
+ arch_config = _load_yaml(_MODEL_ARCHITECTURES_PATH)
12
+ constants = _load_yaml(_CONSTANTS_PATH)
13
+ resolution_map = constants.get("RESOLUTION_MAP", {})
14
+ architectures = arch_config.get("architectures", {})
15
+ architecture_order = arch_config.get("architecture_order", list(architectures.keys()))
16
+
17
+ result = []
18
+ for arch_name in architecture_order:
19
+ if arch_name not in architectures:
20
+ continue
21
+ arch_data = architectures[arch_name]
22
+ model_type = arch_data.get("model_type", arch_name.lower())
23
+
24
+ default_res = [1024, 1024]
25
+ if model_type in resolution_map:
26
+ resolutions = resolution_map[model_type]
27
+ if resolutions:
28
+ first_key = next(iter(resolutions))
29
+ default_res = resolutions[first_key]
30
+
31
+ result.append({
32
+ "model_architecture": arch_name,
33
+ "default_resolution": default_res,
34
+ })
35
+
36
+ return result
mcp_tools/get_model_features.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_model_features
3
+ Query metadata for a specified model, including supported task types, extended features, and default inference parameters.
4
+ """
5
+
6
+ from .common import (
7
+ _load_yaml,
8
+ _MODEL_LIST_PATH,
9
+ _MODEL_DEFAULTS_PATH,
10
+ _IMAGE_GEN_FEATURES_PATH,
11
+ _MODEL_ARCHITECTURES_PATH,
12
+ _CHAIN_FEATURES_PATH,
13
+ _TASK_DEFINITIONS,
14
+ )
15
+ from .error_schema import make_validation_error, make_not_found_error
16
+
17
+
18
+ def handle_get_model_features(model: str) -> dict:
19
+ """Query metadata for a specified model: supported task types, extended features, and default inference parameters."""
20
+ if not model:
21
+ return make_validation_error(
22
+ "Parameter 'model' is required.",
23
+ missing_fields=["model"],
24
+ )
25
+
26
+ model_list = _load_yaml(_MODEL_LIST_PATH)
27
+ model_defaults = _load_yaml(_MODEL_DEFAULTS_PATH)
28
+ features_config = _load_yaml(_IMAGE_GEN_FEATURES_PATH)
29
+ arch_config = _load_yaml(_MODEL_ARCHITECTURES_PATH)
30
+ chain_features = _load_yaml(_CHAIN_FEATURES_PATH)
31
+
32
+ found_arch = None
33
+ checkpoints = model_list.get("Checkpoint", {})
34
+ for arch_name, arch_data in checkpoints.items():
35
+ if not isinstance(arch_data, dict):
36
+ continue
37
+ for m in arch_data.get("models", []):
38
+ if m.get("display_name") == model:
39
+ found_arch = arch_name
40
+ break
41
+ if found_arch:
42
+ break
43
+
44
+ if not found_arch:
45
+ return make_not_found_error("model", model)
46
+
47
+ architectures = arch_config.get("architectures", {})
48
+ arch_info = architectures.get(found_arch, {})
49
+ model_type = arch_info.get("model_type", found_arch.lower())
50
+
51
+ arch_features = features_config.get(model_type, features_config.get("default", {}))
52
+ enabled_chains = arch_features.get("enabled_chains", [])
53
+
54
+ supported_features = []
55
+ for chain_name in enabled_chains:
56
+ if chain_name in chain_features:
57
+ chain_data = chain_features[chain_name]
58
+ visibility = chain_data.get("visibility", "public")
59
+ if visibility == "public":
60
+ supported_features.append(chain_name)
61
+ else:
62
+ generic_mapping = {
63
+ "krea2_controlnet": "controlnet",
64
+ "anima_controlnet_lllite": "controlnet",
65
+ "controlnet_model_patch": "controlnet",
66
+ "flux1_ipadapter": "ipadapter",
67
+ "sd3_ipadapter": "ipadapter",
68
+ "hidream_o1_reference": "reference_latent",
69
+ }
70
+ generic_name = generic_mapping.get(chain_name)
71
+ if generic_name and generic_name not in supported_features:
72
+ supported_features.append(generic_name)
73
+
74
+ arch_defaults_section = model_defaults.get(found_arch, {})
75
+ arch_level_defaults = arch_defaults_section.get("_defaults", {})
76
+ model_specific_defaults = arch_defaults_section.get(model, {})
77
+ global_defaults = model_defaults.get("Default", {})
78
+
79
+ merged_defaults = {**global_defaults, **arch_level_defaults, **model_specific_defaults}
80
+
81
+ default_parameter = {
82
+ "sampler": merged_defaults.get("sampler_name", "euler"),
83
+ "scheduler": merged_defaults.get("scheduler", "simple"),
84
+ "steps": merged_defaults.get("steps", 20),
85
+ "cfg": merged_defaults.get("cfg", 1.0),
86
+ }
87
+
88
+ supported_tasks = [t["task_type"] for t in _TASK_DEFINITIONS]
89
+
90
+ result = {
91
+ "name": model,
92
+ "model_architecture": found_arch,
93
+ "supported_tasks": supported_tasks,
94
+ "supported_features": supported_features,
95
+ "default_parameter": default_parameter,
96
+ }
97
+
98
+ default_pos = model_specific_defaults.get(
99
+ "positive_prompt",
100
+ arch_level_defaults.get("positive_prompt", ""),
101
+ )
102
+ default_neg = model_specific_defaults.get(
103
+ "negative_prompt",
104
+ arch_level_defaults.get("negative_prompt", ""),
105
+ )
106
+ if default_pos:
107
+ result["default_positive_prompt"] = default_pos
108
+ if default_neg:
109
+ result["default_negative_prompt"] = default_neg
110
+
111
+ return result
mcp_tools/get_model_list.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_model_list
3
+ Query the list of available image generation models, with optional filtering by model architecture.
4
+ """
5
+
6
+ from .common import _load_yaml, _MODEL_LIST_PATH, _MODEL_DEFAULTS_PATH, _MODEL_ARCHITECTURES_PATH
7
+ from .error_schema import make_not_found_error
8
+
9
+
10
+ def handle_get_model_list(model_architecture: str = None) -> list | dict:
11
+ """Dynamically load the list of available image generation models from model_list.yaml."""
12
+ model_list = _load_yaml(_MODEL_LIST_PATH)
13
+ model_defaults = _load_yaml(_MODEL_DEFAULTS_PATH)
14
+ arch_config = _load_yaml(_MODEL_ARCHITECTURES_PATH)
15
+ valid_architectures = set(arch_config.get("architectures", {}).keys())
16
+
17
+ if model_architecture and model_architecture not in valid_architectures:
18
+ return make_not_found_error("architecture", model_architecture)
19
+
20
+ result = []
21
+ checkpoints = model_list.get("Checkpoint", {})
22
+
23
+ for arch_name, arch_data in checkpoints.items():
24
+ if model_architecture and arch_name != model_architecture:
25
+ continue
26
+ if not isinstance(arch_data, dict):
27
+ continue
28
+
29
+ models = arch_data.get("models", [])
30
+ if not isinstance(models, list):
31
+ continue
32
+
33
+ arch_defaults = model_defaults.get(arch_name, {})
34
+ arch_level_defaults = arch_defaults.get("_defaults", {})
35
+
36
+ for model in models:
37
+ display_name = model.get("display_name", "")
38
+ category = model.get("category", None)
39
+
40
+ model_specific_defaults = arch_defaults.get(display_name, {})
41
+
42
+ default_pos = model_specific_defaults.get(
43
+ "positive_prompt",
44
+ arch_level_defaults.get("positive_prompt", ""),
45
+ )
46
+ default_neg = model_specific_defaults.get(
47
+ "negative_prompt",
48
+ arch_level_defaults.get("negative_prompt", ""),
49
+ )
50
+
51
+ entry = {
52
+ "name": display_name,
53
+ "model_architecture": arch_name,
54
+ }
55
+ if category:
56
+ entry["category"] = category
57
+ if default_pos:
58
+ entry["default_positive_prompt"] = default_pos
59
+ if default_neg:
60
+ entry["default_negative_prompt"] = default_neg
61
+
62
+ result.append(entry)
63
+
64
+ return result
mcp_tools/get_task_list.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_task_list
3
+ Get a list of all supported image generation task types along with their required/optional parameter lists.
4
+ """
5
+
6
+ from .common import _TASK_DEFINITIONS
7
+
8
+
9
+ def handle_get_task_list() -> list:
10
+ """Get a list of all supported image generation task types along with their required/optional parameter lists."""
11
+ return _TASK_DEFINITIONS
mcp_tools/get_task_status.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: get_task_status
3
+ Query the processing progress and final results of an async image generation task.
4
+ """
5
+
6
+ from .common import _TASKS_DB
7
+ from .error_schema import make_validation_error, make_not_found_error
8
+
9
+
10
+ def handle_get_task_status(task_id: str) -> dict:
11
+ """Query the processing progress and final results of an async image generation task."""
12
+ if not task_id:
13
+ return make_validation_error(
14
+ "Parameter 'task_id' is required.",
15
+ missing_fields=["task_id"],
16
+ )
17
+
18
+ if task_id not in _TASKS_DB:
19
+ return make_not_found_error("task", task_id)
20
+
21
+ return _TASKS_DB[task_id]
mcp_tools/mcp_gradio_integration.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP & Gradio Integration Module
3
+
4
+ Provides:
5
+ 1. register_high_level_mcp_apis: Expose only 8 high-level abstract API/MCP endpoints (using gr.api without polluting the visual UI structure)
6
+ 2. cleanup_dependencies_api_names: Force cleanup of show_api attribute for non-high-level APIs in dependencies
7
+ 3. patch_gradio_api_suppression: No-op implementation retained for backward compatibility
8
+ """
9
+
10
+ import json
11
+ import gradio as gr
12
+
13
+ from .get_task_list import handle_get_task_list
14
+ from .get_model_architecture_list import handle_get_model_architecture_list
15
+ from .get_model_list import handle_get_model_list
16
+ from .get_feature_list import handle_get_feature_list
17
+ from .get_model_features import handle_get_model_features
18
+ from .get_chain_schema import handle_get_chain_schema
19
+ from .run_imagegen import handle_run_imagegen
20
+ from .get_task_status import handle_get_task_status
21
+
22
+ HIGH_LEVEL_MCP_API_NAMES = {
23
+ "get_task_list",
24
+ "get_model_architecture_list",
25
+ "get_model_list",
26
+ "get_feature_list",
27
+ "get_model_features",
28
+ "run_imagegen",
29
+ "get_task_status",
30
+ "get_chain_schema",
31
+ }
32
+
33
+
34
+ def sanitize_keys(obj):
35
+ """Recursively ensure all dictionary keys are converted to str type to avoid Gradio 5 orjson TypeError: Dict key must be str."""
36
+ if isinstance(obj, dict):
37
+ return {str(k): sanitize_keys(v) for k, v in obj.items()}
38
+ elif isinstance(obj, list):
39
+ return [sanitize_keys(x) for x in obj]
40
+ elif isinstance(obj, tuple):
41
+ return tuple(sanitize_keys(x) for x in obj)
42
+ return obj
43
+
44
+
45
+ def patch_gradio_api_suppression():
46
+ """Retained for backward compatibility (no-op)."""
47
+ pass
48
+
49
+
50
+ def cleanup_dependencies_api_names(demo):
51
+ """
52
+ Clean up residual auto-generated API names in demo.fns and demo.dependencies.
53
+ Force only the 8 high-level abstract MCP APIs to be exposed as public endpoints.
54
+ """
55
+ for fn in demo.fns.values():
56
+ api_name = getattr(fn, "api_name", None)
57
+ if api_name not in HIGH_LEVEL_MCP_API_NAMES:
58
+ fn.show_api = False
59
+
60
+ deps = getattr(demo, "dependencies", None)
61
+ if deps is None and hasattr(demo, "config") and isinstance(demo.config, dict):
62
+ deps = demo.config.get("dependencies", [])
63
+
64
+ if deps:
65
+ for dep in deps:
66
+ if isinstance(dep, dict):
67
+ api_name = dep.get("api_name")
68
+ if api_name not in HIGH_LEVEL_MCP_API_NAMES:
69
+ dep["show_api"] = False
70
+
71
+ print("[MCP Protection] Cleaned up demo dependencies. Suppressed atomic API endpoints.")
72
+
73
+
74
+ def register_high_level_mcp_apis(demo):
75
+ """
76
+ Explicitly register 8 high-level abstract MCP API endpoints on the Gradio demo using gr.api.
77
+ Using gr.api() never adds any visual UI components (such as Row, Textbox, Button, etc.), avoiding duplicate interface rendering.
78
+ """
79
+ def get_task_list() -> list:
80
+ """[Recommended Discovery Flow Step 1] Get a list of all supported image generation task types (txt2img, img2img, inpaint, outpaint, hires_fix) along with their required and optional parameter lists. Recommended flow: get_task_list -> get_model_architecture_list -> get_model_list -> [Path 1: Call run_imagegen directly (pass only required params) | Path 2: Call get_model_features to get official default hyperparams -> run_imagegen]."""
81
+ return sanitize_keys(handle_get_task_list())
82
+
83
+ def get_model_architecture_list() -> list:
84
+ """[Recommended Discovery Flow Step 2] Get a list of all supported model architectures (e.g., SD1.5, SDXL, FLUX, etc.) along with their default resolutions. It is recommended to call this tool before get_model_list to obtain valid model_architecture parameters for precise model filtering."""
85
+ return sanitize_keys(handle_get_model_architecture_list())
86
+
87
+ def get_model_list(model_architecture: str = "") -> list | dict:
88
+ """[Recommended Discovery Flow Step 3] Query the list of available image generation models. After obtaining models, choose one of two paths: 1. [Path 1 (Recommended - Minimal Mode)] Call run_imagegen directly with only required parameters. Do NOT guess steps/cfg/sampler/scheduler from experience; the server will automatically apply the model's optimal default hyperparameters. 2. [Path 2 (Explicit Alignment Mode)] First call get_model_features to query the model's officially recommended hyperparameters, then pass them to run_imagegen."""
89
+ arch = model_architecture.strip() if model_architecture else None
90
+ return sanitize_keys(handle_get_model_list(arch))
91
+
92
+ def get_feature_list() -> list:
93
+ """Get the list of supported advanced features along with their usage constraints and parameter schemas."""
94
+ return sanitize_keys(handle_get_feature_list())
95
+
96
+ def get_model_features(model: str = "") -> dict:
97
+ """Query metadata for the specified model, including supported task types, extended features, and official default inference parameters (steps, cfg, sampler, scheduler). This tool MUST be called when explicitly obtaining a model's optimal default hyperparameters (Path 2). Guessing or fabricating hyperparameters without querying is strictly prohibited."""
98
+ return sanitize_keys(handle_get_model_features(model.strip()))
99
+
100
+ def run_imagegen(json_params: str = "{}") -> dict:
101
+ """[Recommended Discovery Flow Step 4] Unified image generation task execution interface. Supports txt2img, img2img, and other tasks with chainable extended features. [IMPORTANT PARAMETER RULES] Do NOT guess or fabricate inference hyperparameters such as steps, cfg, sampler, scheduler! Path 1 (Recommended): Pass only required parameters (task_type, model, prompt, width, height), leave optional hyperparams empty (server uses optimal defaults). Path 2: If explicit hyperparams are needed, you MUST first call get_model_features to obtain official defaults before passing them."""
102
+ try:
103
+ if isinstance(json_params, dict):
104
+ params = json_params
105
+ else:
106
+ params = json.loads(json_params or "{}")
107
+ except Exception as e:
108
+ return {"error": {"code": "INVALID_JSON", "message": f"Failed to parse JSON params: {e}"}}
109
+ return sanitize_keys(handle_run_imagegen(params))
110
+
111
+ def get_task_status(task_id: str = "") -> dict:
112
+ """Query the progress, status, and final generated results of an async image generation task."""
113
+ return sanitize_keys(handle_get_task_status(task_id.strip()))
114
+
115
+ def get_chain_schema(chain_type: str = "") -> dict:
116
+ """Get the complete parameter schema and usage examples for a specified chain/injector type."""
117
+ return sanitize_keys(handle_get_chain_schema(chain_type.strip()))
118
+
119
+ funcs = [
120
+ get_task_list,
121
+ get_model_architecture_list,
122
+ get_model_list,
123
+ get_feature_list,
124
+ get_model_features,
125
+ run_imagegen,
126
+ get_task_status,
127
+ get_chain_schema,
128
+ ]
129
+
130
+ for func in funcs:
131
+ gr.api(func)
132
+
133
+ for fn in demo.fns.values():
134
+ if getattr(fn, "api_name", None) in HIGH_LEVEL_MCP_API_NAMES:
135
+ fn.show_api = True
136
+
137
+ print("[MCP Integration] Successfully registered 8 High-Level Abstract MCP APIs via gr.api().")
mcp_tools/run_imagegen.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool: run_imagegen
3
+ Unified image generation task submission and execution interface.
4
+ """
5
+
6
+ import time
7
+ import uuid
8
+ import threading
9
+ from .common import (
10
+ _load_yaml,
11
+ _MODEL_LIST_PATH,
12
+ _TASK_DEFINITIONS,
13
+ _TASKS_DB,
14
+ _execute_imagegen_pipeline,
15
+ )
16
+ from .error_schema import make_validation_error, make_not_found_error
17
+
18
+
19
+ def handle_run_imagegen(params: dict) -> dict:
20
+ """Unified image generation task execution interface."""
21
+ if not isinstance(params, dict):
22
+ return make_validation_error("Request params must be an object.")
23
+
24
+ missing = []
25
+ for req_field in ["task_type", "model", "prompt"]:
26
+ if req_field not in params or not params[req_field]:
27
+ missing.append(req_field)
28
+ if missing:
29
+ return make_validation_error(
30
+ f"Missing required parameter(s): {', '.join(missing)}",
31
+ missing_fields=missing,
32
+ )
33
+
34
+ task_type = params["task_type"]
35
+ valid_tasks = [t["task_type"] for t in _TASK_DEFINITIONS]
36
+ if task_type not in valid_tasks:
37
+ return make_validation_error(
38
+ f"Invalid task_type '{task_type}'. Must be one of {valid_tasks}.",
39
+ invalid_fields={"task_type": f"Must be in {valid_tasks}"},
40
+ )
41
+
42
+ model_list = _load_yaml(_MODEL_LIST_PATH)
43
+ checkpoints = model_list.get("Checkpoint", {})
44
+ all_models = set()
45
+ for arch_name, arch_data in checkpoints.items():
46
+ if isinstance(arch_data, dict):
47
+ for m in arch_data.get("models", []):
48
+ all_models.add(m.get("display_name"))
49
+
50
+ if params["model"] not in all_models:
51
+ return make_not_found_error("model", params["model"])
52
+
53
+ task_id = f"img_task_{uuid.uuid4().hex[:10]}"
54
+ created_at = int(time.time())
55
+
56
+ _TASKS_DB[task_id] = {
57
+ "task_id": task_id,
58
+ "status": "queued",
59
+ "progress": 0,
60
+ "created_at": created_at,
61
+ }
62
+
63
+ async_exec = params.get("async_execution", False)
64
+
65
+ if async_exec:
66
+ t = threading.Thread(target=_execute_imagegen_pipeline, args=(task_id, params), daemon=True)
67
+ t.start()
68
+ return {
69
+ "status": "queued",
70
+ "task_id": task_id,
71
+ "poll_interval_ms": 2000,
72
+ "message": "Task queued successfully. Poll get_task_status for results.",
73
+ }
74
+ else:
75
+ _execute_imagegen_pipeline(task_id, params)
76
+ return _TASKS_DB[task_id]
mcp_tools/tool_handlers.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Tool Handlers — Backward-compatible aggregation entry point.
3
+ Core logic has been split into individual files (get_*.py and run_imagegen.py).
4
+ """
5
+
6
+ from .get_task_list import handle_get_task_list
7
+ from .get_model_architecture_list import handle_get_model_architecture_list
8
+ from .get_model_list import handle_get_model_list
9
+ from .get_feature_list import handle_get_feature_list
10
+ from .get_model_features import handle_get_model_features
11
+ from .get_chain_schema import handle_get_chain_schema
12
+ from .run_imagegen import handle_run_imagegen
13
+ from .get_task_status import handle_get_task_status
14
+ from .common import (
15
+ _TASK_DEFINITIONS,
16
+ _TASKS_DB,
17
+ _load_yaml,
18
+ _execute_imagegen_pipeline,
19
+ )
20
+
21
+ __all__ = [
22
+ "handle_get_task_list",
23
+ "handle_get_model_architecture_list",
24
+ "handle_get_model_list",
25
+ "handle_get_feature_list",
26
+ "handle_get_model_features",
27
+ "handle_get_chain_schema",
28
+ "handle_run_imagegen",
29
+ "handle_get_task_status",
30
+ ]
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- comfyui-frontend-package==1.47.11
2
- comfyui-workflow-templates==0.11.23
3
  comfyui-embedded-docs==0.5.9
4
  torch
5
  torchsde
@@ -23,7 +23,7 @@ SQLAlchemy>=2.0.0
23
  filelock
24
  av>=16.0.0
25
  comfy-kitchen==0.2.26
26
- comfy-aimdo==0.4.10
27
  requests
28
  simpleeval>=1.0.0
29
  blake3
 
1
+ comfyui-frontend-package==1.47.12
2
+ comfyui-workflow-templates==0.11.27
3
  comfyui-embedded-docs==0.5.9
4
  torch
5
  torchsde
 
23
  filelock
24
  av>=16.0.0
25
  comfy-kitchen==0.2.26
26
+ comfy-aimdo==0.4.11
27
  requests
28
  simpleeval>=1.0.0
29
  blake3
ui/events/__init__.py CHANGED
@@ -1,12 +1,12 @@
1
- from .main import attach_event_handlers
2
- from .config_loaders import (
3
- load_controlnet_config,
4
- get_cn_defaults,
5
- load_anima_controlnet_lllite_config,
6
- get_anima_cn_defaults,
7
- load_diffsynth_controlnet_config,
8
- get_diffsynth_cn_defaults,
9
- load_krea2_controlnet_config,
10
- get_krea2_cn_defaults,
11
- load_ipadapter_config
12
  )
 
1
+ from .main import attach_event_handlers
2
+ from .config_loaders import (
3
+ load_controlnet_config,
4
+ get_cn_defaults,
5
+ load_anima_controlnet_lllite_config,
6
+ get_anima_cn_defaults,
7
+ load_diffsynth_controlnet_config,
8
+ get_diffsynth_cn_defaults,
9
+ load_krea2_controlnet_config,
10
+ get_krea2_cn_defaults,
11
+ load_ipadapter_config
12
  )
ui/events/config_loaders.py CHANGED
@@ -1,168 +1,168 @@
1
- import os
2
- import yaml
3
- from functools import lru_cache
4
-
5
- @lru_cache(maxsize=1)
6
- def load_controlnet_config():
7
- _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
- _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'controlnet_models.yaml')
9
- try:
10
- print("--- Loading controlnet_models.yaml ---")
11
- with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
12
- config = yaml.safe_load(f)
13
- print("--- ✅ controlnet_models.yaml loaded successfully ---")
14
- return config.get("ControlNet", {})
15
- except Exception as e:
16
- print(f"Error loading controlnet_models.yaml: {e}")
17
- return {}
18
-
19
-
20
- def get_cn_defaults(arch_val):
21
- cn_full_config = load_controlnet_config()
22
- cn_config = cn_full_config.get(arch_val, [])
23
-
24
- if not cn_config:
25
- return [], None, [], None, "None"
26
-
27
- all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
28
- default_type = all_types[0] if all_types else None
29
-
30
- series_choices = []
31
- if default_type:
32
- series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
33
- default_series = series_choices[0] if series_choices else None
34
-
35
- filepath = "None"
36
- if default_series and default_type:
37
- for model in cn_config:
38
- if model.get("Series") == default_series and default_type in model.get("Type", []):
39
- filepath = model.get("Filepath")
40
- break
41
-
42
- return all_types, default_type, series_choices, default_series, filepath
43
-
44
-
45
- @lru_cache(maxsize=1)
46
- def load_anima_controlnet_lllite_config():
47
- _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
48
- _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'anima_controlnet_lllite_models.yaml')
49
- try:
50
- print("--- Loading anima_controlnet_lllite_models.yaml ---")
51
- with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
52
- config = yaml.safe_load(f)
53
- print("--- ✅ anima_controlnet_lllite_models.yaml loaded successfully ---")
54
- return config.get("Anima_ControlNet_Lllite", [])
55
- except Exception as e:
56
- print(f"Error loading anima_controlnet_lllite_models.yaml: {e}")
57
- return []
58
-
59
-
60
- def get_anima_cn_defaults():
61
- cn_config = load_anima_controlnet_lllite_config()
62
- if not cn_config:
63
- return [], None, [], None, "None"
64
- all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
65
- default_type = all_types[0] if all_types else None
66
- series_choices = []
67
- if default_type:
68
- series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
69
- default_series = series_choices[0] if series_choices else None
70
- filepath = "None"
71
- if default_series and default_type:
72
- for model in cn_config:
73
- if model.get("Series") == default_series and default_type in model.get("Type", []):
74
- filepath = model.get("Filepath")
75
- break
76
- return all_types, default_type, series_choices, default_series, filepath
77
-
78
-
79
- @lru_cache(maxsize=1)
80
- def load_diffsynth_controlnet_config():
81
- _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
82
- _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'diffsynth_controlnet_models.yaml')
83
- try:
84
- print("--- Loading diffsynth_controlnet_models.yaml ---")
85
- with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
86
- config = yaml.safe_load(f)
87
- print("--- ✅ diffsynth_controlnet_models.yaml loaded successfully ---")
88
- return config.get("DiffSynth_ControlNet", {})
89
- except Exception as e:
90
- print(f"Error loading diffsynth_controlnet_models.yaml: {e}")
91
- return {}
92
-
93
-
94
- def get_diffsynth_cn_defaults(arch_val):
95
- cn_full_config = load_diffsynth_controlnet_config()
96
- cn_config = cn_full_config.get(arch_val, [])
97
-
98
- if not cn_config:
99
- return [], None, [], None, "None"
100
-
101
- all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
102
- default_type = all_types[0] if all_types else None
103
-
104
- series_choices = []
105
- if default_type:
106
- series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
107
- default_series = series_choices[0] if series_choices else None
108
-
109
- filepath = "None"
110
- if default_series and default_type:
111
- for model in cn_config:
112
- if model.get("Series") == default_series and default_type in model.get("Type", []):
113
- filepath = model.get("Filepath")
114
- break
115
-
116
- return all_types, default_type, series_choices, default_series, filepath
117
-
118
-
119
- @lru_cache(maxsize=1)
120
- def load_krea2_controlnet_config():
121
- _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
122
- _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'krea2_controlnet_models.yaml')
123
- try:
124
- print("--- Loading krea2_controlnet_models.yaml ---")
125
- with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
126
- config = yaml.safe_load(f)
127
- print("--- ✅ krea2_controlnet_models.yaml loaded successfully ---")
128
- return config.get("Krea2_ControlNet", [])
129
- except Exception as e:
130
- print(f"Error loading krea2_controlnet_models.yaml: {e}")
131
- return []
132
-
133
- def get_krea2_cn_defaults():
134
- cn_config = load_krea2_controlnet_config()
135
- if not cn_config:
136
- return [], None, [], None, "None"
137
-
138
- all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
139
- default_type = all_types[0] if all_types else None
140
-
141
- series_choices = []
142
- if default_type:
143
- series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
144
- default_series = series_choices[0] if series_choices else None
145
-
146
- filepath = "None"
147
- if default_series and default_type:
148
- for model in cn_config:
149
- if model.get("Series") == default_series and default_type in model.get("Type", []):
150
- filepath = model.get("Filepath")
151
- break
152
-
153
- return all_types, default_type, series_choices, default_series, filepath
154
-
155
-
156
- @lru_cache(maxsize=1)
157
- def load_ipadapter_config():
158
- _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
159
- _IPA_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'ipadapter.yaml')
160
- try:
161
- print("--- Loading ipadapter.yaml ---")
162
- with open(_IPA_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
163
- config = yaml.safe_load(f)
164
- print("--- ✅ ipadapter.yaml loaded successfully ---")
165
- return config
166
- except Exception as e:
167
- print(f"Error loading ipadapter.yaml: {e}")
168
  return {}
 
1
+ import os
2
+ import yaml
3
+ from functools import lru_cache
4
+
5
+ @lru_cache(maxsize=1)
6
+ def load_controlnet_config():
7
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+ _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'controlnet_models.yaml')
9
+ try:
10
+ print("--- Loading controlnet_models.yaml ---")
11
+ with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
12
+ config = yaml.safe_load(f)
13
+ print("--- ✅ controlnet_models.yaml loaded successfully ---")
14
+ return config.get("ControlNet", {})
15
+ except Exception as e:
16
+ print(f"Error loading controlnet_models.yaml: {e}")
17
+ return {}
18
+
19
+
20
+ def get_cn_defaults(arch_val):
21
+ cn_full_config = load_controlnet_config()
22
+ cn_config = cn_full_config.get(arch_val, [])
23
+
24
+ if not cn_config:
25
+ return [], None, [], None, "None"
26
+
27
+ all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
28
+ default_type = all_types[0] if all_types else None
29
+
30
+ series_choices = []
31
+ if default_type:
32
+ series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
33
+ default_series = series_choices[0] if series_choices else None
34
+
35
+ filepath = "None"
36
+ if default_series and default_type:
37
+ for model in cn_config:
38
+ if model.get("Series") == default_series and default_type in model.get("Type", []):
39
+ filepath = model.get("Filepath")
40
+ break
41
+
42
+ return all_types, default_type, series_choices, default_series, filepath
43
+
44
+
45
+ @lru_cache(maxsize=1)
46
+ def load_anima_controlnet_lllite_config():
47
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
48
+ _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'anima_controlnet_lllite_models.yaml')
49
+ try:
50
+ print("--- Loading anima_controlnet_lllite_models.yaml ---")
51
+ with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
52
+ config = yaml.safe_load(f)
53
+ print("--- ✅ anima_controlnet_lllite_models.yaml loaded successfully ---")
54
+ return config.get("Anima_ControlNet_Lllite", [])
55
+ except Exception as e:
56
+ print(f"Error loading anima_controlnet_lllite_models.yaml: {e}")
57
+ return []
58
+
59
+
60
+ def get_anima_cn_defaults():
61
+ cn_config = load_anima_controlnet_lllite_config()
62
+ if not cn_config:
63
+ return [], None, [], None, "None"
64
+ all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
65
+ default_type = all_types[0] if all_types else None
66
+ series_choices = []
67
+ if default_type:
68
+ series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
69
+ default_series = series_choices[0] if series_choices else None
70
+ filepath = "None"
71
+ if default_series and default_type:
72
+ for model in cn_config:
73
+ if model.get("Series") == default_series and default_type in model.get("Type", []):
74
+ filepath = model.get("Filepath")
75
+ break
76
+ return all_types, default_type, series_choices, default_series, filepath
77
+
78
+
79
+ @lru_cache(maxsize=1)
80
+ def load_diffsynth_controlnet_config():
81
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
82
+ _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'diffsynth_controlnet_models.yaml')
83
+ try:
84
+ print("--- Loading diffsynth_controlnet_models.yaml ---")
85
+ with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
86
+ config = yaml.safe_load(f)
87
+ print("--- ✅ diffsynth_controlnet_models.yaml loaded successfully ---")
88
+ return config.get("DiffSynth_ControlNet", {})
89
+ except Exception as e:
90
+ print(f"Error loading diffsynth_controlnet_models.yaml: {e}")
91
+ return {}
92
+
93
+
94
+ def get_diffsynth_cn_defaults(arch_val):
95
+ cn_full_config = load_diffsynth_controlnet_config()
96
+ cn_config = cn_full_config.get(arch_val, [])
97
+
98
+ if not cn_config:
99
+ return [], None, [], None, "None"
100
+
101
+ all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
102
+ default_type = all_types[0] if all_types else None
103
+
104
+ series_choices = []
105
+ if default_type:
106
+ series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
107
+ default_series = series_choices[0] if series_choices else None
108
+
109
+ filepath = "None"
110
+ if default_series and default_type:
111
+ for model in cn_config:
112
+ if model.get("Series") == default_series and default_type in model.get("Type", []):
113
+ filepath = model.get("Filepath")
114
+ break
115
+
116
+ return all_types, default_type, series_choices, default_series, filepath
117
+
118
+
119
+ @lru_cache(maxsize=1)
120
+ def load_krea2_controlnet_config():
121
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
122
+ _CN_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'krea2_controlnet_models.yaml')
123
+ try:
124
+ print("--- Loading krea2_controlnet_models.yaml ---")
125
+ with open(_CN_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
126
+ config = yaml.safe_load(f)
127
+ print("--- ✅ krea2_controlnet_models.yaml loaded successfully ---")
128
+ return config.get("Krea2_ControlNet", [])
129
+ except Exception as e:
130
+ print(f"Error loading krea2_controlnet_models.yaml: {e}")
131
+ return []
132
+
133
+ def get_krea2_cn_defaults():
134
+ cn_config = load_krea2_controlnet_config()
135
+ if not cn_config:
136
+ return [], None, [], None, "None"
137
+
138
+ all_types = sorted(list(set(t for model in cn_config for t in model.get("Type", []))))
139
+ default_type = all_types[0] if all_types else None
140
+
141
+ series_choices = []
142
+ if default_type:
143
+ series_choices = sorted(list(set(model.get("Series", "Default") for model in cn_config if default_type in model.get("Type", []))))
144
+ default_series = series_choices[0] if series_choices else None
145
+
146
+ filepath = "None"
147
+ if default_series and default_type:
148
+ for model in cn_config:
149
+ if model.get("Series") == default_series and default_type in model.get("Type", []):
150
+ filepath = model.get("Filepath")
151
+ break
152
+
153
+ return all_types, default_type, series_choices, default_series, filepath
154
+
155
+
156
+ @lru_cache(maxsize=1)
157
+ def load_ipadapter_config():
158
+ _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
159
+ _IPA_MODEL_LIST_PATH = os.path.join(_PROJECT_ROOT, 'yaml', 'ipadapter.yaml')
160
+ try:
161
+ print("--- Loading ipadapter.yaml ---")
162
+ with open(_IPA_MODEL_LIST_PATH, 'r', encoding='utf-8') as f:
163
+ config = yaml.safe_load(f)
164
+ print("--- ✅ ipadapter.yaml loaded successfully ---")
165
+ return config
166
+ except Exception as e:
167
+ print(f"Error loading ipadapter.yaml: {e}")
168
  return {}
ui/layout.py CHANGED
@@ -37,4 +37,22 @@ def build_ui(event_handler_function):
37
 
38
  event_handler_function(ui_components, demo)
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  return demo
 
37
 
38
  event_handler_function(ui_components, demo)
39
 
40
+ try:
41
+ import mcp_tools as mcp
42
+ if hasattr(mcp, "register_high_level_mcp_apis"):
43
+ mcp.register_high_level_mcp_apis(demo)
44
+ mcp.cleanup_dependencies_api_names(demo)
45
+ elif hasattr(mcp, 'MCP_FUNCTIONS') and isinstance(mcp.MCP_FUNCTIONS, list):
46
+ for func in mcp.MCP_FUNCTIONS:
47
+ gr.api(func)
48
+ print(f"✅ Registered MCP API endpoint: '{func.__name__}'")
49
+ except Exception as e:
50
+ print(f"⚠️ Warning registering MCP functions: {e}")
51
+
52
+ # Disable API exposure for all atomic UI event handlers
53
+ high_level_names = getattr(mcp, "HIGH_LEVEL_MCP_API_NAMES", set())
54
+ for fn in demo.fns.values():
55
+ if getattr(fn, "api_name", None) not in high_level_names:
56
+ fn.show_api = False
57
+
58
  return demo
yaml/image_gen_features.yaml CHANGED
@@ -1,200 +1,200 @@
1
- # Feature names in enabled_chains correspond 1-to-1 with chain_injectors/<name>_injector.py
2
- krea-2:
3
- enabled_chains:
4
- - lora
5
- - krea2_controlnet
6
- - krea2_identity_edit
7
- - krea2_style_reference
8
- - pid
9
-
10
- mage-flow:
11
- enabled_chains:
12
- - reference_image
13
-
14
- joyai-image:
15
- enabled_chains:
16
- - joyai_image
17
-
18
- boogu-image:
19
- enabled_chains:
20
- - lora
21
- - boogu_image_edit
22
- - pid
23
-
24
- pixeldit:
25
- enabled_chains:
26
- - conditioning
27
-
28
- ideogram-4:
29
- enabled_chains:
30
- - vae
31
- - pid
32
-
33
- lens:
34
- enabled_chains:
35
- - conditioning
36
- - pid
37
-
38
- flux2-kv:
39
- enabled_chains:
40
- - lora
41
- - reference_latent
42
- - conditioning
43
- - vae
44
- - pid
45
-
46
- flux2:
47
- enabled_chains:
48
- - lora
49
- - reference_latent
50
- - conditioning
51
- - vae
52
- - pid
53
-
54
- ernie-image:
55
- enabled_chains:
56
- - conditioning
57
- - pid
58
-
59
- z-image:
60
- enabled_chains:
61
- - lora
62
- - diffsynth_controlnet
63
- - conditioning
64
- - vae
65
- - pid
66
-
67
- qwen-image:
68
- enabled_chains:
69
- - lora
70
- - conditioning
71
- - controlnet
72
- - qwen_image_edit
73
- - vae
74
- - pid
75
-
76
- longcat-image:
77
- enabled_chains:
78
- - lora
79
- - conditioning
80
- - pid
81
-
82
- cosmos-predict2:
83
- enabled_chains:
84
- - conditioning
85
- - vae
86
-
87
- anima:
88
- enabled_chains:
89
- - lora
90
- - conditioning
91
- - anima_controlnet_lllite
92
- - vae
93
- - pid
94
-
95
- newbie-image:
96
- enabled_chains:
97
- - lora
98
- - embedding
99
- - conditioning
100
- - vae
101
- - pid
102
-
103
- kandinsky-5:
104
- enabled_chains:
105
- - conditioning
106
- - vae
107
- - pid
108
-
109
- ovis-image:
110
- enabled_chains:
111
- - conditioning
112
- - vae
113
- - pid
114
-
115
- hunyuanimage:
116
- enabled_chains:
117
- - conditioning
118
- - vae
119
-
120
- chroma1-radiance:
121
- enabled_chains:
122
- - conditioning
123
-
124
- chroma1:
125
- enabled_chains:
126
- - conditioning
127
- - vae
128
- - pid
129
-
130
- omnigen2:
131
- enabled_chains:
132
- - reference_latent
133
- - conditioning
134
- - pid
135
-
136
- lumina:
137
- enabled_chains:
138
- - lora
139
- - embedding
140
- - conditioning
141
- - vae
142
- - pid
143
-
144
- hidream-o1:
145
- enabled_chains:
146
- - lora
147
- - conditioning
148
- - hidream_o1_smoothing
149
- - hidream_o1_reference
150
-
151
- hidream-i1:
152
- enabled_chains:
153
- - lora
154
- - conditioning
155
- - pid
156
-
157
- flux1:
158
- enabled_chains:
159
- - lora
160
- - flux1_ipadapter
161
- - conditioning
162
- - style
163
- - controlnet
164
- - vae
165
- - pid
166
-
167
- auraflow:
168
- enabled_chains:
169
- - lora
170
- - conditioning
171
- - vae
172
-
173
- sd35:
174
- enabled_chains:
175
- - lora
176
- - sd3_ipadapter
177
- - embedding
178
- - conditioning
179
- - controlnet
180
- - vae
181
- - pid
182
-
183
- sdxl:
184
- enabled_chains:
185
- - lora
186
- - ipadapter
187
- - embedding
188
- - conditioning
189
- - controlnet
190
- - vae
191
- - pid
192
-
193
- sd15:
194
- enabled_chains:
195
- - lora
196
- - ipadapter
197
- - embedding
198
- - conditioning
199
- - controlnet
200
  - vae
 
1
+ # Feature names in enabled_chains correspond 1-to-1 with chain_injectors/<name>_injector.py
2
+ krea-2:
3
+ enabled_chains:
4
+ - lora
5
+ - krea2_controlnet
6
+ - krea2_identity_edit
7
+ - krea2_style_reference
8
+ - pid
9
+
10
+ mage-flow:
11
+ enabled_chains:
12
+ - reference_image
13
+
14
+ joyai-image:
15
+ enabled_chains:
16
+ - joyai_image
17
+
18
+ boogu-image:
19
+ enabled_chains:
20
+ - lora
21
+ - boogu_image_edit
22
+ - pid
23
+
24
+ pixeldit:
25
+ enabled_chains:
26
+ - conditioning
27
+
28
+ ideogram-4:
29
+ enabled_chains:
30
+ - vae
31
+ - pid
32
+
33
+ lens:
34
+ enabled_chains:
35
+ - conditioning
36
+ - pid
37
+
38
+ flux2-kv:
39
+ enabled_chains:
40
+ - lora
41
+ - reference_latent
42
+ - conditioning
43
+ - vae
44
+ - pid
45
+
46
+ flux2:
47
+ enabled_chains:
48
+ - lora
49
+ - reference_latent
50
+ - conditioning
51
+ - vae
52
+ - pid
53
+
54
+ ernie-image:
55
+ enabled_chains:
56
+ - conditioning
57
+ - pid
58
+
59
+ z-image:
60
+ enabled_chains:
61
+ - lora
62
+ - diffsynth_controlnet
63
+ - conditioning
64
+ - vae
65
+ - pid
66
+
67
+ qwen-image:
68
+ enabled_chains:
69
+ - lora
70
+ - conditioning
71
+ - controlnet
72
+ - qwen_image_edit
73
+ - vae
74
+ - pid
75
+
76
+ longcat-image:
77
+ enabled_chains:
78
+ - lora
79
+ - conditioning
80
+ - pid
81
+
82
+ cosmos-predict2:
83
+ enabled_chains:
84
+ - conditioning
85
+ - vae
86
+
87
+ anima:
88
+ enabled_chains:
89
+ - lora
90
+ - conditioning
91
+ - anima_controlnet_lllite
92
+ - vae
93
+ - pid
94
+
95
+ newbie-image:
96
+ enabled_chains:
97
+ - lora
98
+ - embedding
99
+ - conditioning
100
+ - vae
101
+ - pid
102
+
103
+ kandinsky-5:
104
+ enabled_chains:
105
+ - conditioning
106
+ - vae
107
+ - pid
108
+
109
+ ovis-image:
110
+ enabled_chains:
111
+ - conditioning
112
+ - vae
113
+ - pid
114
+
115
+ hunyuanimage:
116
+ enabled_chains:
117
+ - conditioning
118
+ - vae
119
+
120
+ chroma1-radiance:
121
+ enabled_chains:
122
+ - conditioning
123
+
124
+ chroma1:
125
+ enabled_chains:
126
+ - conditioning
127
+ - vae
128
+ - pid
129
+
130
+ omnigen2:
131
+ enabled_chains:
132
+ - reference_latent
133
+ - conditioning
134
+ - pid
135
+
136
+ lumina:
137
+ enabled_chains:
138
+ - lora
139
+ - embedding
140
+ - conditioning
141
+ - vae
142
+ - pid
143
+
144
+ hidream-o1:
145
+ enabled_chains:
146
+ - lora
147
+ - conditioning
148
+ - hidream_o1_smoothing
149
+ - hidream_o1_reference
150
+
151
+ hidream-i1:
152
+ enabled_chains:
153
+ - lora
154
+ - conditioning
155
+ - pid
156
+
157
+ flux1:
158
+ enabled_chains:
159
+ - lora
160
+ - flux1_ipadapter
161
+ - conditioning
162
+ - style
163
+ - controlnet
164
+ - vae
165
+ - pid
166
+
167
+ auraflow:
168
+ enabled_chains:
169
+ - lora
170
+ - conditioning
171
+ - vae
172
+
173
+ sd35:
174
+ enabled_chains:
175
+ - lora
176
+ - sd3_ipadapter
177
+ - embedding
178
+ - conditioning
179
+ - controlnet
180
+ - vae
181
+ - pid
182
+
183
+ sdxl:
184
+ enabled_chains:
185
+ - lora
186
+ - ipadapter
187
+ - embedding
188
+ - conditioning
189
+ - controlnet
190
+ - vae
191
+ - pid
192
+
193
+ sd15:
194
+ enabled_chains:
195
+ - lora
196
+ - ipadapter
197
+ - embedding
198
+ - conditioning
199
+ - controlnet
200
  - vae