197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
import requests
|
|
import base64
|
|
import torch
|
|
import numpy as np
|
|
from PIL import Image
|
|
import io
|
|
|
|
class GPTImageEditNode:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"prompt": ("STRING", {
|
|
"multiline": True,
|
|
"default": "Edit this image"
|
|
}),
|
|
"api_key": ("STRING", {
|
|
"default": ""
|
|
}),
|
|
"background": (["auto", "transparent", "opaque"], {
|
|
"default": "auto"
|
|
}),
|
|
"quality": (["auto", "high", "medium", "low"], {
|
|
"default": "auto"
|
|
}),
|
|
"size": (["auto", "1024x1024", "1536x1024", "1024x1536"], {
|
|
"default": "auto"
|
|
}),
|
|
"output_format": (["png", "jpeg", "webp"], {
|
|
"default": "png"
|
|
}),
|
|
"output_compression": ("INT", {
|
|
"default": 100,
|
|
"min": 0,
|
|
"max": 100,
|
|
"step": 1
|
|
}),
|
|
"n_images": ("INT", {
|
|
"default": 1,
|
|
"min": 1,
|
|
"max": 10,
|
|
"step": 1
|
|
}),
|
|
},
|
|
"optional": {
|
|
"mask": ("MASK",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("image",)
|
|
FUNCTION = "edit_image"
|
|
CATEGORY = "image/edit"
|
|
|
|
def tensor_to_pil(self, tensor):
|
|
if len(tensor.shape) == 3:
|
|
array = (tensor.cpu().numpy() * 255).astype(np.uint8)
|
|
return Image.fromarray(array)
|
|
else:
|
|
raise ValueError(f"Expected 3D tensor, got {len(tensor.shape)}D")
|
|
|
|
def pil_to_tensor(self, image):
|
|
array = np.array(image).astype(np.float32) / 255.0
|
|
tensor = torch.from_numpy(array).unsqueeze(0)
|
|
|
|
return tensor
|
|
|
|
def mask_to_pil(self, mask):
|
|
if len(mask.shape) == 3 and mask.shape[0] == 1:
|
|
mask = mask.squeeze(0)
|
|
|
|
array = (mask.cpu().numpy() * 255).astype(np.uint8)
|
|
|
|
mask_gray = Image.fromarray(array, mode='L')
|
|
mask_rgba = mask_gray.convert("RGBA")
|
|
mask_rgba.putalpha(mask_gray)
|
|
|
|
return mask_rgba
|
|
|
|
def invert_mask(self, mask_image):
|
|
"""Invert mask image pixel values"""
|
|
array = np.array(mask_image)
|
|
inverted_array = 255 - array
|
|
return Image.fromarray(inverted_array, mode=mask_image.mode)
|
|
|
|
def pil_to_bytes(self, image, format="png"):
|
|
"""Convert PIL Image to bytes buffer"""
|
|
buffer = io.BytesIO()
|
|
|
|
if format.lower() == 'jpeg':
|
|
image = image.convert('RGB')
|
|
|
|
image.save(buffer, format=format.upper())
|
|
buffer.seek(0)
|
|
|
|
return buffer
|
|
|
|
def edit_image(self, image, prompt, api_key, background, quality, size,
|
|
output_format, output_compression, n_images, mask=None):
|
|
|
|
try:
|
|
# Prepare the request
|
|
url = "https://api.openai.com/v1/images/edits"
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}"
|
|
}
|
|
|
|
# Prepare form data
|
|
files = []
|
|
data = {
|
|
"model": "gpt-image-1",
|
|
"prompt": prompt,
|
|
"background": background,
|
|
"n": str(n_images),
|
|
"output_compression": str(output_compression),
|
|
"output_format": output_format,
|
|
"quality": quality,
|
|
"size": size
|
|
}
|
|
|
|
# Handle batched images - send all images as reference
|
|
batch_size = image.shape[0]
|
|
print(f"Processing {batch_size} images as reference")
|
|
|
|
for i in range(batch_size):
|
|
# Get single image from batch
|
|
single_image = image[i]
|
|
pil_image = self.tensor_to_pil(single_image)
|
|
image_buffer = self.pil_to_bytes(pil_image, "png")
|
|
|
|
# Add to form data
|
|
files.append(('image[]', (
|
|
f'input_{i}.png',
|
|
image_buffer,
|
|
'image/png'
|
|
)))
|
|
|
|
# Add mask if provided
|
|
if mask is not None:
|
|
pil_mask = self.mask_to_pil(mask)
|
|
inverted_mask = self.invert_mask(pil_mask)
|
|
mask_buffer = self.pil_to_bytes(inverted_mask, "png")
|
|
files.append(('mask', (
|
|
'mask.png',
|
|
mask_buffer,
|
|
'image/png'
|
|
)))
|
|
|
|
# Make the API request
|
|
response = requests.post(url, headers=headers, data=data, files=files)
|
|
|
|
# Check response
|
|
if response.status_code != 200:
|
|
error_msg = f"API Error {response.status_code}: {response.text}"
|
|
print(f"GPT Image Edit Error: {error_msg}")
|
|
# Return first image from batch on error
|
|
return (image[0:1],)
|
|
|
|
# Parse response
|
|
result = response.json()
|
|
|
|
# Process the first generated image
|
|
if result['data']:
|
|
b64_json = result['data'][0]['b64_json']
|
|
image_bytes = base64.b64decode(b64_json)
|
|
|
|
# Convert to PIL Image
|
|
pil_image = Image.open(io.BytesIO(image_bytes))
|
|
|
|
# Convert back to tensor
|
|
output_tensor = self.pil_to_tensor(pil_image)
|
|
|
|
return (output_tensor,)
|
|
else:
|
|
print("No images returned from API")
|
|
return (image[0:1],)
|
|
|
|
except Exception as e:
|
|
print(f"GPT Image Edit Error: {str(e)}")
|
|
# Return first image from batch on error
|
|
return (image[0:1],)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
# Always re-execute when prompt changes
|
|
return kwargs.get("prompt", "")
|
|
|
|
# Node registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"GPTImageEditNode": GPTImageEditNode
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"GPTImageEditNode": "GPT Image Edit"
|
|
} |