25 Commits
Author SHA1 Message Date
Sander 613466e53d Merge pull request #5 from brantje/load-llava-on-demand
Load llava on demand
2025-05-20 21:31:49 +02:00
brantje 65819fd372 Update version to 1.0.4 in pyproject.toml for ComfyUI-MagicQuill 2025-05-20 19:28:33 +00:00
brantje f1e2c00c04 Refactor LLaVAModel initialization and add unload_model method for better resource management
- Introduced unload_model method to clear GPU memory and reset model-related attributes after use.
- Modified MagicQuill to load and unload the LLaVAModel during prompt guessing, enhancing memory efficiency.
2025-05-20 19:27:58 +00:00
brantje a1950e4ac5 Refactor VALIDATE_INPUTS method in MagicQuill to include optional_image parameter and update version to 1.0.3 in pyproject.toml 2025-05-10 07:48:53 +00:00
brantje 1e7423d1d1 Fix importing brushnet 2025-05-09 18:29:22 +00:00
Sander 9f0bfc4d99 Merge pull request #4 from brantje/inputs
Add optional inputs
2025-05-06 00:13:53 +02:00
brantje b746b5d7d4 Update version to 1.0.1 in pyproject.toml for ComfyUI-MagicQuill 2025-05-05 22:12:39 +00:00
brantje f6e84847a1 Improve tensor handling for optional images in MagicQuill
- Added a check to assign optional_image directly if it is a tensor, enhancing flexibility in image processing.
- Updated conditional logic to ensure proper handling of string inputs for various image parameters, improving robustness in image preparation.
2025-05-05 21:38:38 +00:00
brantje b9969ec6c2 Enhance optional image handling in MagicQuill
- Added new parameters for optional image masks in the painter_execute method to improve image processing capabilities.
- Updated the handling of optional images and masks, including resizing logic for mismatched dimensions.
- Refined print statements to include additional optional parameters for better debugging.
2025-05-05 21:25:17 +00:00
brantje 8b82f3c9eb Refactor optional image parameters in MagicQuill and update widget settings
- Commented out previous optional image parameters in favor of new structured parameters: optional_image, optional_original_image, optional_add_color_image, optional_add_edge_mask, and optional_remove_edge_mask.
- Updated the painter_execute method to accept the new parameters and handle image processing accordingly.
- Adjusted widget settings in extension.js and magicquill_widget.js to reflect the changes in optional parameters.
2025-05-05 20:48:50 +00:00
brantje 1d3033cc94 Add optional image parameters to MagicQuill and update widget settings
- Introduced optional parameters for image handling in the MagicQuill class: optional_image_name, optional_original_image_name, optional_add_color_image_name, optional_add_edge_image_name, and optional_remove_edge_image_name.
- Updated the painter_execute method to utilize these optional parameters for enhanced image processing.
- Modified the magicquill_widget.js to include the new optional image parameters in the settings widget.
2025-05-05 20:06:32 +00:00
Sander 4f4602a53f Merge pull request #3 from brantje/remove-node-packs
Remove repository cloning code from install.py and update README.md t…
2025-04-30 08:51:48 +02:00
brantje af680e8696 Remove repository cloning code from install.py and update README.md to include notes on required repositories. 2025-04-30 06:50:55 +00:00
brantje 0c7a4ff3a5 Update transformers dependency in requirements.txt to version 4.38.0 2025-04-26 07:38:14 +00:00
Sander f680891251 Update pyproject.toml 2025-04-24 14:58:39 +02:00
Sander b63fb8746a Update pyproject.toml 2025-04-24 14:56:47 +02:00
Sander e118268866 Update README.md 2025-04-24 14:56:05 +02:00
Sander 360c073bd1 Create main.yml 2025-04-24 14:49:55 +02:00
Sander 4b608ba8d8 Update pyproject.toml 2025-04-24 14:49:11 +02:00
brantje 672404c008 Update webcolors dependency in requirements.txt and refactor closest_colour function in utils.py to use name_to_rgb method for improved color matching. 2025-04-21 21:07:40 +00:00
brantje 8743d0f666 Enable repository cloning in install.py by uncommenting the relevant code block, allowing for automatic cloning of specified repositories if they do not already exist. 2025-04-18 11:17:15 +00:00
brantje 8066c4e453 Refactor guess_prompt_handler in magic_quill.py to improve image input handling and error management. Added base64 support for images, enhanced validation, and ensured proper cleanup of temporary files after processing. 2025-04-18 07:31:10 +00:00
brantje e22c6e56ff Add random seed generation in run_magic_quill function to enhance variability in outputs. Default seed now uses a randomly generated value if not provided in the request. 2025-04-17 22:43:16 +00:00
Sander b5d26fb5be Merge pull request #2 from brantje/api-call
Api call
2025-04-17 21:59:07 +02:00
Sander 0cff7ce040 Merge pull request #1 from brantje/fixes
Fixes
2025-04-17 21:57:09 +02:00
11 changed files with 293 additions and 83 deletions
+20
View File
@@ -0,0 +1,20 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
+13
View File
@@ -4,6 +4,19 @@
<a href="https://huggingface.co/spaces/AI4Editing/MagicQuill"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)"></a> <a href="https://huggingface.co/spaces/AI4Editing/MagicQuill"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)"></a>
<a href="https://creativecommons.org/licenses/by-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg"></a> <a href="https://creativecommons.org/licenses/by-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg"></a>
# Fixed
- [x] SD1.5
Todo
- [ ] SDXL
- [ ] FLUX
Note:
[Brushnet](https://github.com/nullquant/ComfyUI-BrushNet) and [ComfyUI ControlNet Aux](https://github.com/Fannovel16/comfyui_controlnet_aux) are required.
https://github.com/user-attachments/assets/8ee9663a-fef2-484a-a0b7-8427ab590424 https://github.com/user-attachments/assets/8ee9663a-fef2-484a-a0b7-8427ab590424
There is an HD video on [Youtube](https://www.youtube.com/watch?v=5DiKfONMnE4). There is an HD video on [Youtube](https://www.youtube.com/watch?v=5DiKfONMnE4).
-10
View File
@@ -8,13 +8,3 @@ print(models_dir)
# Download the model from the Hugging Face Hub # Download the model from the Hugging Face Hub
repo_id = "LiuZichen/MagicQuill-models" # or any other model/dataset ID repo_id = "LiuZichen/MagicQuill-models" # or any other model/dataset ID
snapshot_download(repo_id=repo_id, local_dir=models_dir) snapshot_download(repo_id=repo_id, local_dir=models_dir)
repos = {
'ComfyUI_BrushNet': 'https://github.com/nullquant/ComfyUI-BrushNet',
'comfyui_controlnet_aux': 'https://github.com/Fannovel16/comfyui_controlnet_aux'
}
# for name, url in repos.items():
# target_dir = os.path.join(parent_dir, name)
# if not os.path.exists(target_dir):
# subprocess.run(['git', 'clone', url, target_dir], check=True)
+1 -1
View File
@@ -28,7 +28,7 @@ app.registerExtension({
for (const w of this.widgets) { for (const w of this.widgets) {
if (["collapse_setting", "clear_canvas"].includes(w.name)) { if (["collapse_setting", "clear_canvas"].includes(w.name)) {
// always show these widgets // always show these widgets
} else if (["image", "original_image", "add_color_image", "add_edge_image", "remove_edge_image", "positive_prompt"].includes(w.name)) { } else if (["image", "original_image", "add_color_image", "add_edge_image", "remove_edge_image"].includes(w.name)) {
w.type = "hidden"; w.type = "hidden";
w.value = null; w.value = null;
w.computeSize = () => [0, -4]; w.computeSize = () => [0, -4];
+1 -1
View File
@@ -266,7 +266,7 @@ export function MaigcQuillWidget(node, inputName, inputData, app) {
}; };
node.onDrawBackground = (ctx) => { node.onDrawBackground = (ctx) => {
const setting_widgets = ["base_model_version", "negative_prompt", "dtype", "grow_size", "stroke_as_edge", "fine_edge", "edge_strength", "color_strength", "inpaint_strength", "seed", "steps", "cfg", "sampler_name", "scheduler"] const setting_widgets = ["base_model_version","positive_prompt", "negative_prompt", "dtype", "grow_size", "stroke_as_edge", "fine_edge", "edge_strength", "color_strength", "inpaint_strength", "seed", "steps", "cfg", "sampler_name", "scheduler", "optional_original_image_name", "optional_add_color_image_name", "optional_add_edge_image_name", "optional_remove_edge_image_name"]
if (!this.flags.setting_collapsed) { if (!this.flags.setting_collapsed) {
for (const w of this.widgets) { for (const w of this.widgets) {
if (setting_widgets.includes(w.name)) { if (setting_widgets.includes(w.name)) {
+21 -6
View File
@@ -23,15 +23,30 @@ import re
class LLaVAModel: class LLaVAModel:
def __init__(self): def __init__(self):
# replace the model_path with correct path folder # replace the model_path with correct path folder
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) self.base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
models_dir = os.path.join(base_path, "models") self.models_dir = os.path.join(self.base_path, "models")
model_path = os.path.join(models_dir, "llava-v1.5-7b-finetune-clean") self.model_path = os.path.join(self.models_dir, "llava-v1.5-7b-finetune-clean")
self.tokenizer = None
self.model = None
self.image_processor = None
self.context_len = None
def load_model(self):
self.tokenizer, self.model, self.image_processor, self.context_len = load_pretrained_model( self.tokenizer, self.model, self.image_processor, self.context_len = load_pretrained_model(
model_path=model_path, model_path=self.model_path,
model_base=None, model_base=None,
model_name=get_model_name_from_path(model_path), model_name=get_model_name_from_path(self.model_path),
) )
def unload_model(self):
"""Unload the model and clear GPU memory."""
if self.model is not None:
self.model.cpu()
del self.model
torch.cuda.empty_cache()
self.tokenizer = None
self.image_processor = None
self.context_len = None
def generate_description(self, images, question): def generate_description(self, images, question):
qs = question qs = question
+126 -27
View File
@@ -1,6 +1,7 @@
import hashlib import hashlib
import os import os
import json import json
import random
from server import PromptServer from server import PromptServer
from PIL import Image, ImageOps from PIL import Image, ImageOps
import torch import torch
@@ -89,25 +90,92 @@ async def process_background_img(request):
@PromptServer.instance.routes.post("/magic_quill/guess_prompt") @PromptServer.instance.routes.post("/magic_quill/guess_prompt")
async def guess_prompt_handler(request): async def guess_prompt_handler(request):
json_data = await request.json() json_data = await request.json()
add_color_image = json_data.get("add_color_image", None) add_color_image_data = json_data.get("add_color_image", None)
original_image = json_data.get("original_image", None) original_image_data = json_data.get("original_image", None)
add_edge_image = json_data.get("add_edge_image", None) add_edge_image_data = json_data.get("add_edge_image", None)
original_image_path = folder_paths.get_annotated_filepath(original_image) if not original_image_data:
original_image_tensor = load_and_preprocess_image(original_image_path) return web.json_response({"error": "Original image is required."}, status=400)
if add_color_image:
add_color_image_path = folder_paths.get_annotated_filepath(add_color_image)
add_color_image_tensor = load_and_preprocess_image(add_color_image_path)
else:
add_color_image_tensor = original_image_tensor
width, height = original_image_tensor.shape[1], original_image_tensor.shape[2]
add_edge_mask = create_alpha_mask(folder_paths.get_annotated_filepath(add_edge_image)) if add_edge_image else torch.zeros((1, height, width), dtype=torch.float32, device="cpu")
res = MagicQuill.guess_prompt(original_image_tensor, add_color_image_tensor, add_edge_mask) temp_files_to_clean = []
input_dir = folder_paths.get_input_directory()
return web.json_response({"prompt": res, "error": False}) def is_base64(s):
return isinstance(s, str) and s.startswith("data:image/")
def handle_image_input(image_data, filename_base):
if is_base64(image_data):
try:
image = read_base64_image(image_data)
timestamp = int(time.time())
# Use a short hash to minimize collision chance but keep filename reasonable
hash_part = hashlib.sha1(str(timestamp).encode() + image_data.encode()).hexdigest()[:8]
filename = f"{filename_base}_{timestamp}_{hash_part}.png"
filepath = os.path.join(input_dir, filename)
image.save(filepath)
print(f"Saved temporary image to {filepath}")
temp_files_to_clean.append(filepath)
return filepath
except Exception as e:
print(f"Error processing base64 image for {filename_base}: {e}")
# Raise or return error? For now, let it raise to signal failure.
raise ValueError(f"Invalid base64 data for {filename_base}") from e
elif isinstance(image_data, str): # Assume filename
return folder_paths.get_annotated_filepath(image_data)
else: # Handle None or other invalid types
return None
try:
original_image_path = handle_image_input(original_image_data, "guess_original")
if not original_image_path or not os.path.exists(original_image_path):
return web.json_response({"error": f"Original image not found or invalid: {original_image_data}"}, status=400)
original_image_tensor = load_and_preprocess_image(original_image_path)
add_color_image_path = None
if add_color_image_data:
add_color_image_path = handle_image_input(add_color_image_data, "guess_add_color")
if not add_color_image_path or not os.path.exists(add_color_image_path):
print(f"Warning: Add color image specified but not found or invalid: {add_color_image_data}. Using original image.")
add_color_image_path = None # Fallback
if add_color_image_path:
add_color_image_tensor = load_and_preprocess_image(add_color_image_path)
else:
add_color_image_tensor = original_image_tensor # Fallback to original
add_edge_image_path = None
if add_edge_image_data:
add_edge_image_path = handle_image_input(add_edge_image_data, "guess_add_edge")
if not add_edge_image_path or not os.path.exists(add_edge_image_path):
print(f"Warning: Add edge image specified but not found or invalid: {add_edge_image_data}. Ignoring add edge.")
add_edge_image_path = None # Ignore if invalid
width, height = original_image_tensor.shape[2], original_image_tensor.shape[1] # Corrected order W, H
add_edge_mask = create_alpha_mask(add_edge_image_path) if add_edge_image_path else torch.zeros((1, height, width), dtype=torch.float32, device="cpu")
# Ensure mask dimensions match original image
if add_edge_mask.shape[1] != height or add_edge_mask.shape[2] != width:
add_edge_mask = F.interpolate(add_edge_mask.unsqueeze(0), size=(height, width), mode='nearest').squeeze(0)
res = MagicQuill.guess_prompt(original_image_tensor, add_color_image_tensor, add_edge_mask)
return web.json_response({"prompt": res, "error": False})
except Exception as e:
import traceback
traceback_str = traceback.format_exc()
print(f"Error in guess_prompt_handler: {str(e)}\nTraceback: {traceback_str}")
return web.json_response({"error": str(e), "traceback": traceback_str}, status=500)
finally:
# Cleanup temporary files
for filepath in temp_files_to_clean:
try:
if os.path.exists(filepath):
os.remove(filepath)
print(f"Cleaned up temporary file: {filepath}")
except Exception as e:
print(f"Warning: Error cleaning up temporary file {filepath}: {str(e)}")
@PromptServer.instance.routes.post("/magic_quill/run") @PromptServer.instance.routes.post("/magic_quill/run")
async def run_magic_quill(request): async def run_magic_quill(request):
@@ -194,6 +262,8 @@ async def run_magic_quill(request):
clip = out[1] clip = out[1]
vae = out[2] vae = out[2]
random_seed = random.randint(0, 0xffffffffffffffff)
if not model or not vae or not clip: if not model or not vae or not clip:
return web.json_response({"error": "Missing required model objects"}, status=400) return web.json_response({"error": "Missing required model objects"}, status=400)
@@ -207,7 +277,8 @@ async def run_magic_quill(request):
edge_strength = post.get("edge_strength", 0.5) edge_strength = post.get("edge_strength", 0.5)
color_strength = post.get("color_strength", 0.5) color_strength = post.get("color_strength", 0.5)
inpaint_strength = post.get("inpaint_strength", 1.0) inpaint_strength = post.get("inpaint_strength", 1.0)
seed = post.get("seed", 0)
seed = post.get("seed", random_seed) #Random seed
steps = post.get("steps", 20) steps = post.get("steps", 20)
cfg = post.get("cfg", 4.0) cfg = post.get("cfg", 4.0)
sampler_name = post.get("sampler_name", "euler_ancestral") sampler_name = post.get("sampler_name", "euler_ancestral")
@@ -239,7 +310,7 @@ async def run_magic_quill(request):
steps=steps, steps=steps,
cfg=cfg, cfg=cfg,
sampler_name=sampler_name, sampler_name=sampler_name,
scheduler=scheduler scheduler=scheduler,
) )
# Convert the result tensors to base64 # Convert the result tensors to base64
@@ -326,9 +397,18 @@ class MagicQuill(object):
"cfg": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01, "display": "slider"}), "cfg": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01, "display": "slider"}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler_ancestral"}), "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler_ancestral"}),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "exponential"}), "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "exponential"}),
# "optional_original_image_name": ("STRING", {"default": ""}),
# "optional_add_color_image_name": ("STRING", {"default": ""}),
# "optional_add_edge_image_name": ("STRING", {"default": ""}),
# "optional_remove_edge_image_name": ("STRING", {"default": ""}),
}, },
"optional": { "optional": {
"optional_image": ("IMAGE",),
"optional_image_mask": ("MASK",),
"optional_original_image": ("IMAGE",),
"optional_add_color_image": ("IMAGE",),
"optional_add_edge_mask": ("MASK",),
"optional_remove_edge_mask": ("MASK",),
} }
} }
@@ -341,6 +421,7 @@ class MagicQuill(object):
@classmethod @classmethod
def prepare_images_and_masks(cls, image, original_image, add_color_image, add_edge_image, remove_edge_image): def prepare_images_and_masks(cls, image, original_image, add_color_image, add_edge_image, remove_edge_image):
# Handle file path inputs # Handle file path inputs
print(f"image: {image} original_image: {original_image} add_color_image: {add_color_image} add_edge_image: {add_edge_image} remove_edge_image: {remove_edge_image}")
image_path = folder_paths.get_annotated_filepath(image) image_path = folder_paths.get_annotated_filepath(image)
image_tensor = load_and_preprocess_image(image_path) image_tensor = load_and_preprocess_image(image_path)
height, width = image_tensor.shape[1], image_tensor.shape[2] height, width = image_tensor.shape[1], image_tensor.shape[2]
@@ -373,20 +454,42 @@ class MagicQuill(object):
@classmethod @classmethod
def guess_prompt(cls, original_image_tensor, add_color_image_tensor, add_edge_mask): def guess_prompt(cls, original_image_tensor, add_color_image_tensor, add_edge_mask):
cls.llavaModel.load_model()
description, ans1, ans2 = cls.llavaModel.process(original_image_tensor, add_color_image_tensor, add_edge_mask) description, ans1, ans2 = cls.llavaModel.process(original_image_tensor, add_color_image_tensor, add_edge_mask)
ans_list = [] ans_list = []
if ans1 and ans1 != "": if ans1 and ans1 != "":
ans_list.append(ans1) ans_list.append(ans1)
if ans2 and ans2 != "": if ans2 and ans2 != "":
ans_list.append(ans2) ans_list.append(ans2)
cls.llavaModel.unload_model()
return ", ".join(ans_list) return ", ".join(ans_list)
@classmethod @classmethod
def painter_execute(cls, image, original_image, add_color_image, add_edge_image, remove_edge_image, model, vae, clip, base_model_version, positive_prompt, negative_prompt, dtype, grow_size, stroke_as_edge, fine_edge, edge_strength, color_strength, inpaint_strength, seed, steps, cfg, sampler_name, scheduler): def painter_execute(cls, image, original_image, add_color_image, add_edge_image, remove_edge_image, model, vae, clip, base_model_version, positive_prompt, negative_prompt, dtype, grow_size, stroke_as_edge, fine_edge, edge_strength, color_strength, inpaint_strength, seed, steps, cfg, sampler_name, scheduler, optional_image = None, optional_image_mask = None, optional_original_image = None, optional_original_image_mask = None, optional_add_color_image = None, optional_add_color_image_mask = None, optional_add_edge_mask = None, optional_add_edge_mask_mask = None, optional_remove_edge_mask = None, optional_remove_edge_mask_mask = None):
print(f"model: {model} vae: {vae} clip: {clip} base_model_version: {base_model_version} positive_prompt: {positive_prompt} negative_prompt: {negative_prompt} dtype: {dtype} grow_size: {grow_size} stroke_as_edge: {stroke_as_edge} fine_edge: {fine_edge} edge_strength: {edge_strength} color_strength: {color_strength} inpaint_strength: {inpaint_strength} seed: {seed} steps: {steps} cfg: {cfg} sampler_name: {sampler_name} scheduler: {scheduler}") print(f"model: {model} vae: {vae} clip: {clip} base_model_version: {base_model_version} positive_prompt: {positive_prompt} negative_prompt: {negative_prompt} dtype: {dtype} grow_size: {grow_size} stroke_as_edge: {stroke_as_edge} fine_edge: {fine_edge} edge_strength: {edge_strength} color_strength: {color_strength} inpaint_strength: {inpaint_strength} seed: {seed} steps: {steps} cfg: {cfg} sampler_name: {sampler_name} scheduler: {scheduler}")
print(f"original_image: {original_image} add_color_image: {add_color_image} add_edge_image: {add_edge_image} remove_edge_image: {remove_edge_image}") print(f"original_image: {original_image} add_color_image: {add_color_image} add_edge_image: {add_edge_image} remove_edge_image: {remove_edge_image}")
add_color_image, original_image, total_mask, add_edge_mask, remove_edge_mask = cls.prepare_images_and_masks(image, original_image, add_color_image, add_edge_image, remove_edge_image) print(f"optional_image: {optional_image} optional_image_mask: {optional_image_mask} optional_original_image: {optional_original_image} optional_original_image_mask: {optional_original_image_mask} optional_add_color_image: {optional_add_color_image} optional_add_color_image_mask: {optional_add_color_image_mask} optional_add_edge_mask: {optional_add_edge_mask} optional_add_edge_mask_mask: {optional_add_edge_mask_mask} optional_remove_edge_mask: {optional_remove_edge_mask} optional_remove_edge_mask_mask: {optional_remove_edge_mask_mask}")
# check if optional_original_image is tensor
if isinstance(optional_image, torch.Tensor):
image = optional_image
if isinstance(optional_original_image, torch.Tensor):
original_image = optional_original_image
if isinstance(optional_add_color_image, torch.Tensor):
add_color_image = optional_add_color_image
if isinstance(optional_add_edge_mask, torch.Tensor):
add_edge_mask = optional_add_edge_mask
if isinstance(optional_remove_edge_mask, torch.Tensor):
remove_edge_mask = optional_remove_edge_mask
if isinstance(optional_image, torch.Tensor) and isinstance(optional_image_mask, torch.Tensor):
#if if not the same size, resize the mask
if optional_image_mask.shape[1] != optional_image.shape[1] or optional_image_mask.shape[2] != optional_image.shape[2]:
print("resizing mask")
optional_image_mask = F.interpolate(optional_image_mask.unsqueeze(0), size=(optional_image.shape[1], optional_image.shape[2]), mode='nearest').squeeze(0)
total_mask = optional_image_mask
if not isinstance(image, torch.Tensor) and not isinstance(original_image, torch.Tensor) and not isinstance(add_color_image, torch.Tensor) and not isinstance(add_edge_image, torch.Tensor) and not isinstance(remove_edge_image, torch.Tensor):
add_color_image, original_image, total_mask, add_edge_mask, remove_edge_mask = cls.prepare_images_and_masks(image, original_image, add_color_image, add_edge_image, remove_edge_image)
if torch.sum(remove_edge_mask).item() > 0 and torch.sum(add_edge_mask).item() == 0: if torch.sum(remove_edge_mask).item() > 0 and torch.sum(add_edge_mask).item() == 0:
if positive_prompt == "": if positive_prompt == "":
@@ -425,9 +528,5 @@ class MagicQuill(object):
return m.digest().hex() return m.digest().hex()
@classmethod @classmethod
def VALIDATE_INPUTS(self, image, original_image, add_color_image, add_edge_image, remove_edge_image, model, vae, clip, base_model_version, positive_prompt, negative_prompt, dtype, grow_size, edge_strength, color_strength, inpaint_strength, seed, steps, cfg, sampler_name, scheduler): def VALIDATE_INPUTS(self, image, original_image, add_color_image, add_edge_image, remove_edge_image, model, vae, clip, base_model_version, positive_prompt, negative_prompt, dtype, grow_size, edge_strength, color_strength, inpaint_strength, seed, steps, cfg, sampler_name, scheduler, optional_image = None):
if not folder_paths.exists_annotated_filepath(image):
print(image)
return "Invalid image file: {}".format(image)
return True return True
+71 -29
View File
@@ -1,35 +1,77 @@
[tool.poetry]
name = "ComfyUI-MagicQuill"
version = "1.0.4"
description = "Fixed version of the original MagicQuill node."
authors = ["brantje <brantje@gmail.com>"]
license = { text = "MIT License" }
readme = "README.md"
[tool.poetry.dependencies]
pynvml = "^11.4.0"
[build-system] [build-system]
requires = ["setuptools>=61.0"] requires = ["poetry-core"]
build-backend = "setuptools.build_meta" build-backend = "poetry.core.masonry.api"
[project] [project]
name = "llava" name = "comfyui_magicquill_fixed" # Unique identifier for your node. Immutable after creation..
version = "1.2.2.post1" description = "Fixed version of the original MagicQuill node. Required nodes: ComfyUI-Brushnet and ComfyUI Controlnet AUX"
description = "Towards GPT-4 like large language and visual assistant." version = "1.0.4" # Custom Node version. Must be semantically versioned.
readme = "README.md" dependencies = [
requires-python = ">=3.8" 'opencv-python',
classifiers = [ 'diffusers',
"Programming Language :: Python :: 3", 'torchsde',
"License :: OSI Approved :: Apache Software License", 'protobuf',
] 'transformers',
dependencies = [ 'tokenizers',
"transformers==4.37.2", "tokenizers==0.15.1", "sentencepiece==0.2.0", "shortuuid", 'sentencepiece',
"accelerate==0.33.0", "peft", "bitsandbytes", 'shortuuid',
"pydantic", "markdown2[all]", "numpy", "scikit-learn==1.2.2", 'accelerate',
"gradio==5.4.0", "gradio_client==1.4.2", "requests", "httpx==0.24.1", "uvicorn", "fastapi", 'peft',
"einops==0.6.1", "einops-exts==0.0.4", "timm==0.6.13", 'bitsandbytes',
] 'pydantic',
'markdown2',
[project.optional-dependencies] 'scikit-learn',
train = ["deepspeed==0.12.6", "ninja", "wandb"] 'requests',
build = ["build", "twine"] 'httpx',
'uvicorn',
'fastapi',
'einops',
'einops-exts',
'timm',
'webcolors',
'torch',
'importlib_metadata',
'huggingface_hub',
'scipy',
'opencv-python',
'filelock',
'numpy',
'Pillow',
'einops',
'torchvision',
'pyyaml',
'scikit-image',
'python-dateutil',
'mediapipe',
'svglib',
'fvcore',
'yapf',
'omegaconf',
'ftfy',
'addict',
'yacs',
'trimesh[easy]',
'albumentations',
'scikit-learn',
'matplotlib'
] # Filled in from requirements.txt
[project.urls] [project.urls]
"Homepage" = "https://llava-vl.github.io" Repository = "https://github.com/brantje/ComfyUI_MagicQuill"
"Bug Tracker" = "https://github.com/haotian-liu/LLaVA/issues"
[tool.setuptools.packages.find] [tool.comfy]
exclude = ["assets*", "benchmark*", "docs", "dist*", "playground*", "scripts*", "tests*"] PublisherId = "brantje"
DisplayName = "ComfyUI MagicQuill (fixed)"
[tool.wheel]
exclude = ["assets*", "benchmark*", "docs", "dist*", "playground*", "scripts*", "tests*"]
+2 -3
View File
@@ -1,9 +1,8 @@
webcolors==1.13
opencv-python opencv-python
diffusers diffusers
torchsde==0.2.6 torchsde==0.2.6
protobuf==4.25.4 protobuf==4.25.4
transformers==4.37.2 transformers==4.38.0
tokenizers==0.15.1 tokenizers==0.15.1
sentencepiece==0.2.0 sentencepiece==0.2.0
shortuuid shortuuid
@@ -20,7 +19,7 @@ fastapi
einops==0.6.1 einops==0.6.1
einops-exts==0.0.4 einops-exts==0.0.4
timm==0.6.13 timm==0.6.13
webcolors==24.11.1
torch torch
importlib_metadata importlib_metadata
huggingface_hub huggingface_hub
+36 -4
View File
@@ -4,14 +4,46 @@ import torch
import sys import sys
import torch.utils._pytree as pytree import torch.utils._pytree as pytree
import numpy as np import numpy as np
import subprocess
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir) sys.path.append(current_dir)
sys.path.append(os.path.abspath(os.path.join(current_dir, '..'))) custom_nodes_dir = os.path.abspath(os.path.join(current_dir, '..'))
sys.path.append(custom_nodes_dir)
sys.path.append(os.path.abspath(os.path.join(current_dir, '..', '..', 'comfy_extras'))) sys.path.append(os.path.abspath(os.path.join(current_dir, '..', '..', 'comfy_extras')))
print(sys.path)
from ComfyUI_BrushNet.brushnet_nodes import BrushNetLoader, BrushNet, BlendInpaint, get_files_with_extension
brushnet_hyphen_dir = os.path.join(custom_nodes_dir, 'comfyui-brushnet')
brushnet_underscore_dir = os.path.join(custom_nodes_dir, 'comfyui_brushnet')
if not os.path.exists(brushnet_underscore_dir):
print(f"Creating symlink from {brushnet_hyphen_dir} to {brushnet_underscore_dir}")
# Create the symlink - use different methods based on OS
if os.name == 'nt': # Windows
# Requires admin privileges or developer mode
subprocess.run(['mklink', '/D', brushnet_underscore_dir, brushnet_hyphen_dir], shell=True)
else: # Unix/Linux/Mac
os.symlink(brushnet_hyphen_dir, brushnet_underscore_dir)
print(f"Symlink created: {os.path.exists(brushnet_underscore_dir)}")
# Now try importing from the symlinked directory
try:
# Add to path
sys.path.append(custom_nodes_dir)
# Import from symlinked directory
from comfyui_brushnet.brushnet_nodes import BrushNetLoader, BrushNet, BlendInpaint, get_files_with_extension
print("Successfully imported from symlinked directory")
except ImportError as e:
print(f"Import from symlink failed: {e}")
try:
from ComfyUI_BrushNet.brushnet_nodes import BrushNetLoader, BrushNet, BlendInpaint, get_files_with_extension
except ImportError as e:
print(f"Import from ComfyUI_BrushNet failed: {e}")
raise ImportError("Failed to import even with ComfyUI_BrushNet. Please check file permissions and structure.")
from comfyui_controlnet_aux.node_wrappers.lineart import LineArt_Preprocessor from comfyui_controlnet_aux.node_wrappers.lineart import LineArt_Preprocessor
from comfyui_controlnet_aux.node_wrappers.pidinet import PIDINET_Preprocessor from comfyui_controlnet_aux.node_wrappers.pidinet import PIDINET_Preprocessor
from comfyui_controlnet_aux.node_wrappers.color import Color_Preprocessor from comfyui_controlnet_aux.node_wrappers.color import Color_Preprocessor
+2 -2
View File
@@ -34,8 +34,8 @@ def get_colored_contour(img1, img2, threshold=10):
def closest_colour(requested_colour): def closest_colour(requested_colour):
min_colours = {} min_colours = {}
for key, name in webcolors.CSS3_HEX_TO_NAMES.items(): for name in webcolors.names("css3"):
r_c, g_c, b_c = webcolors.hex_to_rgb(key) r_c, g_c, b_c = webcolors.name_to_rgb(name)
rd = (r_c - requested_colour[0].item()) ** 2 rd = (r_c - requested_colour[0].item()) ** 2
gd = (g_c - requested_colour[1].item()) ** 2 gd = (g_c - requested_colour[1].item()) ** 2
bd = (b_c - requested_colour[2].item()) ** 2 bd = (b_c - requested_colour[2].item()) ** 2