Files

186 lines
7.0 KiB
Python

import base64
import io
import numpy as np
import requests
import torch
from PIL import Image
from .aimlapi_shared import aspect_ratio_to_size, tensors_to_data_uri_list, validate_api_key
class AIMLAPIImageEditor:
"""
Image-to-Image editing using Google Nano Banana Pro Edit or ByteDance Seedream 4.5.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {
"multiline": False,
"default": "",
"password": True,
"tooltip": "Your AI/ML API key from https://aimlapi.com/app/keys",
}),
"model": (["google/nano-banana-pro-edit", "bytedance/seedream-4-5"], {
"default": "google/nano-banana-pro-edit",
"tooltip": "Select the image editing model",
}),
"prompt": ("STRING", {
"multiline": True,
"default": "Transform the scene to autumn with colorful falling leaves",
"tooltip": "Text description of how to edit the image",
}),
"image": ("IMAGE", {
"tooltip": "Input image(s) to edit (supports batching)",
}),
},
"optional": {
"aspect_ratio": (["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9", "5:4", "4:5"], {
"default": "1:1",
"tooltip": "Aspect ratio (Nano Banana only)",
}),
"resolution": (["1K", "2K", "4K"], {
"default": "1K",
"tooltip": "Output image resolution",
}),
"num_images": ("INT", {
"default": 1,
"min": 1,
"max": 4,
"step": 1,
"tooltip": "Number of images to generate (Nano Banana only, 1-4)",
}),
"seed": ("INT", {
"default": -1,
"min": -1,
"max": 2147483647,
"tooltip": "Seed for reproducibility (Seedream only, -1 for random)",
}),
"guidance_scale": ("FLOAT", {
"default": 3.5,
"min": 0.0,
"max": 20.0,
"step": 0.1,
"tooltip": "Guidance scale (Seedream only)",
}),
"watermark": ("BOOLEAN", {
"default": False,
"tooltip": "Add invisible watermark (Seedream only)",
}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "edit_image"
CATEGORY = "AI/ML API"
def edit_image(self, api_key, model, prompt, image,
aspect_ratio="1:1", resolution="1K", num_images=1,
seed=-1, guidance_scale=3.5, watermark=False):
api_key = validate_api_key(api_key)
url = "https://api.aimlapi.com/v1/images/generations"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
image_urls = tensors_to_data_uri_list(image, limit=14)
if len(image_urls) > 14:
image_urls = image_urls[:14]
payload = {
"model": model,
"prompt": prompt,
"image_urls": image_urls,
"response_format": "url",
}
if model == "google/nano-banana-pro-edit":
payload["aspect_ratio"] = aspect_ratio
payload["resolution"] = resolution
payload["num_images"] = num_images
elif model == "bytedance/seedream-4-5":
payload["size"] = aspect_ratio_to_size(aspect_ratio, resolution)
payload["watermark"] = watermark
payload["guidance_scale"] = guidance_scale
if seed != -1:
payload["seed"] = seed
try:
response = requests.post(url, headers=headers, json=payload, timeout=120)
if response.status_code != 200:
self._raise_image_error(response)
result = response.json()
image_data_list = None
if "data" in result and result["data"]:
image_data_list = result["data"]
elif "images" in result and result["images"]:
image_data_list = result["images"]
if not image_data_list:
raise ValueError(f"No images returned from API. Response: {result}")
images = []
for img_data in image_data_list:
output_image = self._download_image(img_data)
image_np = np.array(output_image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_np)[None,]
images.append(image_tensor)
output = torch.cat(images, dim=0) if len(images) > 1 else images[0]
return (output,)
except requests.exceptions.RequestException as exc:
raise ValueError(f"Network error: {exc}")
@staticmethod
def _raise_image_error(response):
error_msg = f"API request failed with status {response.status_code}"
try:
error_data = response.json()
if "error" in error_data:
error_msg += f": {error_data['error']}"
elif "message" in error_data:
error_msg += f": {error_data['message']}"
else:
error_msg += f": {error_data}"
except Exception:
error_msg += f": {response.text[:300]}"
if response.status_code == 403:
error_msg += "\n\nTroubleshooting 403 Forbidden:\n"
error_msg += "- Verify your API key is correct at https://aimlapi.com/app/keys\n"
error_msg += "- Check if your API key has sufficient credits\n"
error_msg += "- Ensure your API key hasn't expired\n"
error_msg += "- Try generating a new API key"
elif response.status_code == 401:
error_msg += "\n\nAPI key is invalid or missing. Get a valid key from https://aimlapi.com/app/keys"
elif response.status_code == 429:
error_msg += "\n\nRate limit exceeded. Please wait a moment and try again."
elif response.status_code >= 500:
error_msg += "\n\nAPI server error. Please try again later."
raise ValueError(error_msg)
@staticmethod
def _download_image(img_data):
if "url" in img_data:
img_response = requests.get(img_data["url"], timeout=30)
img_response.raise_for_status()
image = Image.open(io.BytesIO(img_response.content))
elif "b64_json" in img_data and img_data["b64_json"]:
image_bytes = base64.b64decode(img_data["b64_json"])
image = Image.open(io.BytesIO(image_bytes))
else:
raise ValueError(f"No image URL or base64 data in response: {img_data}")
if image.mode != "RGB":
image = image.convert("RGB")
return image