Spaces:
Running on Zero
Running on Zero
Add High-Level API/MCP Tools
Browse files- .gitattributes +35 -35
- app.py +4 -2
- chain_injectors/__init__.py +50 -50
- chain_injectors/boogu_image_edit_injector.py +73 -73
- chain_injectors/joyai_image_injector.py +63 -63
- chain_injectors/krea2_identity_edit_injector.py +173 -173
- chain_injectors/krea2_style_reference_injector.py +168 -201
- chain_injectors/qwen_image_edit_injector.py +113 -113
- chain_injectors/reference_image_injector.py +64 -64
- core/pipelines/workflow_executor.py +134 -134
- core/pipelines/workflow_recipes/_partials/_base_sampler.yaml +27 -27
- core/pipelines/workflow_recipes/_partials/conditioning/boogu-image.yaml +67 -67
- core/pipelines/workflow_recipes/_partials/conditioning/qwen-image.yaml +90 -90
- core/pipelines/workflow_recipes/unified_recipe.yaml +7 -7
- core/workflow_assembler.py +164 -164
- mcp_tools/__init__.py +50 -0
- mcp_tools/common.py +335 -0
- mcp_tools/error_schema.py +85 -0
- mcp_tools/get_chain_schema.py +32 -0
- mcp_tools/get_feature_list.py +29 -0
- mcp_tools/get_model_architecture_list.py +36 -0
- mcp_tools/get_model_features.py +111 -0
- mcp_tools/get_model_list.py +64 -0
- mcp_tools/get_task_list.py +11 -0
- mcp_tools/get_task_status.py +21 -0
- mcp_tools/mcp_gradio_integration.py +137 -0
- mcp_tools/run_imagegen.py +76 -0
- mcp_tools/tool_handlers.py +30 -0
- requirements.txt +3 -3
- ui/events/__init__.py +11 -11
- ui/events/config_loaders.py +167 -167
- ui/layout.py +18 -0
- yaml/image_gen_features.yaml +199 -199
.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(
|
| 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 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 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.
|
| 2 |
-
comfyui-workflow-templates==0.11.
|
| 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.
|
| 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
|