Author SHA1 Message Date
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
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
7 changed files with 195 additions and 60 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)
+88 -17
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")
+71 -29
View File
@@ -1,35 +1,77 @@
[tool.poetry]
name = "ComfyUI-MagicQuill"
version = "1.0.0"
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.0" # 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*"]
+1 -2
View File
@@ -1,4 +1,3 @@
webcolors==1.13
opencv-python opencv-python
diffusers diffusers
torchsde==0.2.6 torchsde==0.2.6
@@ -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
+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