* feat(easy_pattern): add test-pattern node + auto-restart dev-sync Adds an `Easy_Pattern` node (Koolook/Testing) for solid-color frame batches with optional per-frame index overlays — verifies sequence merging, frame insertion, batch indexing, and video-pipeline frame order at a glance. Color inputs use the same `White/Black/Gray/Custom` dropdown idiom `EasyResize_Koolook` already exposes for pad/panel colors, so "Gray" means the same 50% sRGB across nodes. `scripts/sync_to_dev.py` now restarts the live ComfyUI server after a successful sync (via ComfyUI-Manager's `/manager/reboot`). Default-on because dev-sync is only ever invoked to see new code, and Python custom-node files load once at server start — `--no-restart` is the escape hatch. The `RUNTIME_PATHS` manifest also picks up `k_easy_pattern.py` so adding a new top-level node doesn't silently break the pack at import time on the next sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sync-dev): restrict restart URL to http(s) for Bandit B310 `urllib.request.urlopen` accepts arbitrary schemes including `file://`, which Bandit B310 flags as a path-traversal vector. The script's typical caller is the maintainer's own shell, but a typo in `--restart-url` could surprise — and the static gate is right to flag it. Validate the scheme is http/https before the urlopen call and annotate it so Bandit accepts the now-truthful narrowing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
193 lines
6.6 KiB
Python
193 lines
6.6 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# ComfyUI-Koolook — Easy Pattern (Koolook variant)
|
|
# Copyright (C) 2026 ComfyUI-Koolook contributors (kforgelabs).
|
|
#
|
|
# This file is part of ComfyUI-Koolook, licensed under GPL-3.0-or-later.
|
|
# See the LICENSE file at the repo root for the full text.
|
|
#
|
|
# Originally drafted in an external AI-assisted chat session as a small
|
|
# standalone MIT-licensed test node, then ingested into ComfyUI-Koolook
|
|
# and relicensed under the project's GPL-3.0-or-later. No third-party
|
|
# upstream repository — this is in-house code.
|
|
|
|
"""
|
|
Easy Pattern node for ComfyUI-Koolook.
|
|
|
|
Generates a batch of solid-color images, optionally stamped with a per-frame
|
|
index number. Useful for testing sequence merging, frame insertion, batch
|
|
indexing, video pipeline order — anywhere a more flexible alternative to
|
|
KJNodes' `ImageBatchTestPattern` is needed.
|
|
"""
|
|
|
|
import os
|
|
import numpy as np
|
|
import torch
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
def _hex_to_rgb(hex_str):
|
|
"""Accepts '#RRGGBB', 'RRGGBB', '#RGB', or a 3-tuple-like string. Falls back to black."""
|
|
if not isinstance(hex_str, str):
|
|
return (0, 0, 0)
|
|
s = hex_str.strip().lstrip("#")
|
|
try:
|
|
if len(s) == 3:
|
|
return tuple(int(c * 2, 16) for c in s)
|
|
if len(s) == 6:
|
|
return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))
|
|
except ValueError:
|
|
pass
|
|
return (0, 0, 0)
|
|
|
|
|
|
# 50% Gray = sRGB (128, 128, 128); matches the `Gray` option in
|
|
# `EasyResize_Koolook`'s `pad_color_mode` and `easy_ImageBatch`'s
|
|
# `placeholder_color`, so the meaning of "Gray" is consistent across nodes.
|
|
PRESET_RGB = {
|
|
"White": (255, 255, 255),
|
|
"Black": (0, 0, 0),
|
|
"Gray": (128, 128, 128),
|
|
}
|
|
|
|
|
|
def _resolve_color(mode, custom_hex):
|
|
"""Mode wins; custom hex is only consulted when mode == 'Custom'."""
|
|
if mode in PRESET_RGB:
|
|
return PRESET_RGB[mode]
|
|
return _hex_to_rgb(custom_hex)
|
|
|
|
|
|
def _load_font(size):
|
|
"""Try a few common font paths; fall back to PIL default if none work."""
|
|
candidates = [
|
|
# Linux
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
# macOS
|
|
"/System/Library/Fonts/Helvetica.ttc",
|
|
"/Library/Fonts/Arial.ttf",
|
|
# Windows
|
|
"C:/Windows/Fonts/arialbd.ttf",
|
|
"C:/Windows/Fonts/arial.ttf",
|
|
]
|
|
for path in candidates:
|
|
if os.path.exists(path):
|
|
try:
|
|
return ImageFont.truetype(path, size)
|
|
except OSError:
|
|
continue
|
|
return ImageFont.load_default()
|
|
|
|
|
|
class EasyPattern:
|
|
"""
|
|
Configurable test-pattern batch.
|
|
|
|
Outputs: IMAGE tensor of shape [B, H, W, 3] with values in [0, 1].
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"batch_size": ("INT", {"default": 81, "min": 1, "max": 4096, "step": 1}),
|
|
"width": ("INT", {"default": 512, "min": 16, "max": 8192, "step": 8}),
|
|
"height": ("INT", {"default": 512, "min": 16, "max": 8192, "step": 8}),
|
|
"bg_color_mode": (["White", "Black", "Gray", "Custom"], {"default": "Custom"}),
|
|
"show_text": ("BOOLEAN", {"default": True}),
|
|
"text_color_mode": (["White", "Black", "Gray", "Custom"], {"default": "White"}),
|
|
"start_from": ("INT", {"default": 1, "min": 0, "max": 999999, "step": 1}),
|
|
"step": ("INT", {"default": 1, "min": -1000, "max": 1000, "step": 1}),
|
|
"font_size": ("INT", {"default": 256, "min": 8, "max": 1024, "step": 1}),
|
|
"position": (
|
|
["center", "top-left", "top-right", "bottom-left", "bottom-right"],
|
|
{"default": "center"},
|
|
),
|
|
"zero_pad": ("INT", {"default": 0, "min": 0, "max": 8, "step": 1}),
|
|
},
|
|
"optional": {
|
|
"bg_color": ("STRING", {"default": "#C71585"}),
|
|
"text_color": ("STRING", {"default": "#FFFFFF"}),
|
|
"prefix": ("STRING", {"default": "", "multiline": False}),
|
|
"suffix": ("STRING", {"default": "", "multiline": False}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("images",)
|
|
FUNCTION = "generate"
|
|
CATEGORY = "Koolook/Testing"
|
|
|
|
def generate(
|
|
self,
|
|
batch_size,
|
|
width,
|
|
height,
|
|
bg_color_mode,
|
|
show_text,
|
|
text_color_mode,
|
|
start_from,
|
|
step,
|
|
font_size,
|
|
position,
|
|
zero_pad=0,
|
|
bg_color="#C71585",
|
|
text_color="#FFFFFF",
|
|
prefix="",
|
|
suffix="",
|
|
):
|
|
bg_rgb = _resolve_color(bg_color_mode, bg_color)
|
|
fg_rgb = _resolve_color(text_color_mode, text_color)
|
|
font = _load_font(font_size) if show_text else None
|
|
|
|
frames = []
|
|
for i in range(batch_size):
|
|
img = Image.new("RGB", (width, height), bg_rgb)
|
|
|
|
if show_text:
|
|
n = start_from + i * step
|
|
num_str = f"{n:0{zero_pad}d}" if zero_pad > 0 else str(n)
|
|
text = f"{prefix}{num_str}{suffix}"
|
|
|
|
draw = ImageDraw.Draw(img)
|
|
# Measure with textbbox so it works on modern Pillow; fall back
|
|
# to the deprecated textsize on very old installs.
|
|
try:
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
bx, by = bbox[0], bbox[1]
|
|
except AttributeError:
|
|
tw, th = draw.textsize(text, font=font)
|
|
bx, by = 0, 0
|
|
|
|
pad = max(8, font_size // 8)
|
|
if position == "center":
|
|
x = (width - tw) // 2 - bx
|
|
y = (height - th) // 2 - by
|
|
elif position == "top-left":
|
|
x, y = pad - bx, pad - by
|
|
elif position == "top-right":
|
|
x, y = width - tw - pad - bx, pad - by
|
|
elif position == "bottom-left":
|
|
x, y = pad - bx, height - th - pad - by
|
|
else: # bottom-right
|
|
x, y = width - tw - pad - bx, height - th - pad - by
|
|
|
|
draw.text((x, y), text, font=font, fill=fg_rgb)
|
|
|
|
arr = np.array(img, dtype=np.float32) / 255.0
|
|
frames.append(arr)
|
|
|
|
batch = np.stack(frames, axis=0) # [B, H, W, 3]
|
|
return (torch.from_numpy(batch),)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"Easy_Pattern": EasyPattern,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"Easy_Pattern": "Easy Pattern (Koolook)",
|
|
}
|