Files
MariusKM-ComfyUI-BadmanNodes/BadmanPromptFileLoader.py
T
MariusKMandClaude Opus 4.6 4c09aa8d71 Add Prompt File Image Loader node
Loads an image + prompt pair from a delimited .txt file by index, so
batch generations can iterate through pre-authored prompt/image lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:34 +02:00

140 lines
4.4 KiB
Python

import os
import numpy as np
import torch
from PIL import Image, ImageOps, ImageSequence
def parse_prompt_file(text, delimiter="---"):
"""Parse a prompt file with the structure:
---
filename.png
---
prompt text (may span multiple lines)
---
filename2.png
---
prompt text2
"""
lines = text.splitlines()
entries = []
i = 0
n = len(lines)
while i < n:
while i < n and lines[i].strip() != delimiter:
i += 1
if i >= n:
break
i += 1
while i < n and lines[i].strip() == "":
i += 1
if i >= n:
break
filename = lines[i].strip()
i += 1
while i < n and lines[i].strip() != delimiter:
i += 1
if i >= n:
break
i += 1
prompt_lines = []
while i < n and lines[i].strip() != delimiter:
prompt_lines.append(lines[i])
i += 1
prompt = "\n".join(prompt_lines).strip()
if filename:
entries.append((filename, prompt))
return entries
def load_image_as_tensor(path):
img = Image.open(path)
output_images = []
output_masks = []
for frame in ImageSequence.Iterator(img):
frame = ImageOps.exif_transpose(frame)
if frame.mode == "I":
frame = frame.point(lambda i: i * (1 / 255))
rgb = frame.convert("RGB")
arr = np.array(rgb).astype(np.float32) / 255.0
output_images.append(torch.from_numpy(arr)[None,])
if "A" in frame.getbands():
mask = np.array(frame.getchannel("A")).astype(np.float32) / 255.0
mask = 1.0 - torch.from_numpy(mask)
else:
mask = torch.zeros((arr.shape[0], arr.shape[1]), dtype=torch.float32)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1:
image = torch.cat(output_images, dim=0)
mask = torch.cat(output_masks, dim=0)
else:
image = output_images[0]
mask = output_masks[0]
return image, mask
class BadmanPromptFileImageLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image_directory": ("STRING", {"default": "", "multiline": False}),
"prompt_file": ("STRING", {"default": "", "multiline": False}),
"index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"delimiter": ("STRING", {"default": "---", "multiline": False}),
"wrap_index": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "INT")
RETURN_NAMES = ("image", "mask", "prompt", "filename", "total")
FUNCTION = "load"
CATEGORY = "Badman"
@classmethod
def IS_CHANGED(cls, image_directory, prompt_file, index, delimiter, wrap_index):
try:
mtime = os.path.getmtime(prompt_file)
except OSError:
mtime = 0
return f"{prompt_file}|{mtime}|{index}|{delimiter}|{wrap_index}|{image_directory}"
def load(self, image_directory, prompt_file, index, delimiter, wrap_index):
if not prompt_file or not os.path.isfile(prompt_file):
raise FileNotFoundError(f"Prompt file not found: {prompt_file}")
if not image_directory or not os.path.isdir(image_directory):
raise NotADirectoryError(f"Image directory not found: {image_directory}")
with open(prompt_file, "r", encoding="utf-8") as f:
text = f.read()
entries = parse_prompt_file(text, delimiter=delimiter)
total = len(entries)
if total == 0:
raise ValueError(f"No entries parsed from prompt file: {prompt_file}")
if wrap_index:
idx = index % total
else:
if index < 0 or index >= total:
raise IndexError(f"Index {index} out of range (0..{total - 1})")
idx = index
filename, prompt = entries[idx]
image_path = os.path.join(image_directory, filename)
if not os.path.isfile(image_path):
raise FileNotFoundError(f"Image not found for entry {idx}: {image_path}")
image, mask = load_image_as_tensor(image_path)
return (image, mask, prompt, filename, total)
NODE_CLASS_MAPPINGS = {
"BadmanPromptFileImageLoader": BadmanPromptFileImageLoader,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BadmanPromptFileImageLoader": "Prompt File Image Loader (Badman)",
}