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>
140 lines
4.4 KiB
Python
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)",
|
|
}
|