Files
gokayfem-ComfyUI-fal-API/nodes/image_node.py
T
Gökay Aydoğan b75f731abf feat: v2.0.0 — full fal catalog coverage, dynamic nodes, core rewrite (#75)
Auto-generated nodes for the entire live fal catalog (1,391 models →
1,482 total nodes) built at startup from a committed registry, plus a
full rewrite of the core layer. All 87 v1 node keys, inputs and outputs
are preserved — existing workflows load and run unchanged.

New:
- data/fal_registry.json + scripts/build_registry.py: distills fal's
  catalog and per-endpoint OpenAPI schemas into a deterministic registry
- nodes/dynamic/: schema→node factory (typed widgets, tooltips, pricing
  in node help, IMAGE/VIDEO/AUDIO auto-upload, native VIDEO/AUDIO
  outputs, seed -1=random, force_rerun), plus a generic Fal Any Endpoint
  node for arbitrary endpoints
- nodes/utils/: rewritten core — FalApiError surfacing fal's real error
  payloads, queue progress logs, ComfyUI cancel support, retries and
  timeouts everywhere, parallel uploads/downloads, structured logging
- tests/ (45 tests incl. a legacy-key snapshot lock), CI lint+test
  workflow, weekly registry-refresh workflow that opens a PR on changes
- README regenerated from the registry (scripts/build_readme.py)

Fixed:
- GPTImage2 / GPTImage2Edit were defined but never registered
- Topaz video upscale sent a nonexistent API field (desired_increase)
- SeedVR video upscaler had its error handling commented out + wrong
  model label; ProRes enum value mismatch translated at argument level
- 10x `is ""` comparisons, bare excepts, missing HTTP timeouts,
  temp-file leaks, fps=0 crash in LoadVideoURL, duplicate enum entry
- opencv-python & friends missing from pyproject (Registry installs)
- API failures now raise visible errors instead of silently returning
  blank images or "Error:" strings that downstream nodes tried to load

BREAKING (behavioral only): failed API calls raise instead of returning
blank/black images or error strings. Node signatures are unchanged.
2026-07-02 12:46:22 +03:00

2540 lines
106 KiB
Python
Executable File

from .fal_utils import ApiHandler, ImageUtils, ResultProcessor, logger
class Sana:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "square_hd", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 3840, "min": 512, "max": 4096, "step": 16, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 2160, "min": 512, "max": 4096, "step": 16, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 18, "min": 1, "max": 50, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": (
"FLOAT",
{"default": 5.0, "min": 1.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
},
"optional": {
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "What to avoid in the generated image"}),
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
"output_format": (["png", "jpeg"], {"default": "png", "tooltip": "File format of the generated image"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
negative_prompt="",
seed=-1,
enable_safety_checker=True,
output_format="png",
):
arguments = {
"prompt": prompt,
"negative_prompt": negative_prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
"output_format": output_format,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/sana", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Sana", e)
class Recraft:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "square_hd", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": ("INT", {"default": 512, "min": 512, "max": 2048, "step": 16, "tooltip": "Output width in pixels (used when image_size is 'custom')"}),
"height": (
"INT",
{"default": 512, "min": 512, "max": 2048, "step": 16, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"style": (
[
"any",
"realistic_image",
"digital_illustration",
"vector_illustration",
"realistic_image/b_and_w",
"realistic_image/hard_flash",
"realistic_image/hdr",
"realistic_image/natural_light",
"realistic_image/studio_portrait",
"realistic_image/enterprise",
"realistic_image/motion_blur",
"digital_illustration/pixel_art",
"digital_illustration/hand_drawn",
"digital_illustration/grain",
"digital_illustration/infantile_sketch",
"digital_illustration/2d_art_poster",
"digital_illustration/handmade_3d",
"digital_illustration/hand_drawn_outline",
"digital_illustration/engraving_color",
"digital_illustration/2d_art_poster_2",
"vector_illustration/engraving",
"vector_illustration/line_art",
"vector_illustration/line_circuit",
"vector_illustration/linocut",
],
{"default": "realistic_image", "tooltip": "Recraft generation style (realistic photo, digital illustration, vector art, ...)"},
),
},
"optional": {
"style_id": ("STRING", {"default": "", "tooltip": "ID of a custom Recraft style; overrides the preset style when set"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(self, prompt, image_size, width, height, style, style_id=""):
arguments = {
"prompt": prompt,
"style": style,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if style_id:
arguments["style_id"] = style_id
try:
result = ApiHandler.submit_and_get_result("fal-ai/recraft-v3", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Recraft", e)
class HidreamFull:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1440, "step": 32, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1440, "step": 32, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 20.0, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
safety_tolerance,
seed=-1,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(
"fal-ai/hidream-i1-full", arguments
)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Hidream Full", e)
class Ideogramv3:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1440, "step": 32, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1440, "step": 32, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 20.0, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
safety_tolerance,
seed=-1,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/ideogram/v3", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Ideogramv3", e)
class FluxPro:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1440, "step": 32, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1440, "step": 32, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 20.0, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
safety_tolerance,
seed=-1,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux-pro", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxPro", e)
class FluxDev:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1536, "step": 16, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1536, "step": 16, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 20.0, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
enable_safety_checker,
seed=-1,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux/dev", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxDev", e)
class FluxSchnell:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1536, "step": 32, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1536, "step": 32, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 4, "min": 1, "max": 100, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
num_images,
enable_safety_checker,
seed=-1,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux/schnell", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxSchnell", e)
class FluxPro11:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1440, "step": 32, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1440, "step": 32, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_images,
safety_tolerance,
seed=-1,
sync_mode=False,
):
arguments = {
"prompt": prompt,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
"sync_mode": sync_mode,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux-pro/v1.1", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxPro 1.1", e)
class FluxPro1Fill:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
"output_format": (["png", "jpeg"], {"default": "png", "tooltip": "File format of the generated image"}),
},
"optional": {
"image": ("IMAGE", {"default": None, "tooltip": "Input image to edit or transform"}),
"mask_image": ("IMAGE", {"default": None, "tooltip": "Inpainting mask: white areas are regenerated, black areas are preserved"}),
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
"enhance_prompt": ("BOOLEAN", {"default": True, "tooltip": "Let the model automatically enhance and expand the prompt"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image = None,
mask_image = None,
output_format = "png",
num_images = 1,
safety_tolerance = "2",
seed=0,
sync_mode=False,
enhance_prompt=True,
):
if image is None or mask_image is None:
return ApiHandler.handle_image_generation_error(
"FluxPro 1/FILL", "Both image and mask_image inputs are required."
)
image_url = ImageUtils.upload_image(image)
mask_url = ImageUtils.upload_image(mask_image)
arguments = {
"prompt": prompt,
"num_images": num_images,
"sync_mode": sync_mode,
"output_format": output_format,
"safety_tolerance": safety_tolerance,
"image_url": image_url,
"mask_url": mask_url,
"enhance_prompt": enhance_prompt,
}
if seed != 0:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux-pro/v1/fill", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxPro 1/FILL", e)
class FluxUltra:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"aspect_ratio": (
["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "9:21"],
{"default": "16:9", "tooltip": "Aspect ratio of the generated image"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 1, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
"raw": ("BOOLEAN", {"default": False, "tooltip": "Generate less processed, more natural-looking images (Flux Ultra raw mode)"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
aspect_ratio,
num_images,
safety_tolerance,
enable_safety_checker,
raw,
sync_mode,
seed=-1,
):
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
"enable_safety_checker": enable_safety_checker,
"raw": raw,
"sync_mode": sync_mode,
}
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(
"fal-ai/flux-pro/v1.1-ultra", arguments
)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxUltra", e)
class FluxLora:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1536, "step": 16, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1536, "step": 16, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 50, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": (
"FLOAT",
{"default": 3.0, "min": 0.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"lora_path_1": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #1 to apply"}),
"lora_scale_1": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #1 (1.0 = full effect)"},
),
"lora_path_2": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #2 to apply"}),
"lora_scale_2": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #2 (1.0 = full effect)"},
),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
enable_safety_checker,
seed=-1,
lora_path_1="",
lora_scale_1=1.0,
lora_path_2="",
lora_scale_2=1.0,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
# Add LoRAs
loras = []
if lora_path_1:
loras.append({"path": lora_path_1, "scale": lora_scale_1})
if lora_path_2:
loras.append({"path": lora_path_2, "scale": lora_scale_2})
if loras:
arguments["loras"] = loras
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux-lora", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxLora", e)
class FluxGeneral:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "landscape_4_3", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 1024, "min": 512, "max": 1536, "step": 16, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 768, "min": 512, "max": 1536, "step": 16, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 50, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": (
"FLOAT",
{"default": 3.0, "min": 0.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"real_cfg_scale": (
"FLOAT",
{"default": 3.3, "min": 0.0, "max": 5.0, "step": 0.1, "tooltip": "Classical CFG scale used when use_real_cfg is enabled"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"enable_safety_checker": ("BOOLEAN", {"default": False, "tooltip": "Run the safety checker on generated images"}),
"use_real_cfg": ("BOOLEAN", {"default": False, "tooltip": "Use classical CFG instead of Flux's distilled guidance"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
"optional": {
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"ip_adapter_scale": (
"FLOAT",
{"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Strength of the IP-Adapter image conditioning"},
),
"controlnet_conditioning_scale": (
"FLOAT",
{"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Strength of the ControlNet conditioning"},
),
"ip_adapters": (
["None", "XLabs-AI/flux-ip-adapter"],
{"default": "None", "tooltip": "IP-Adapter model for image-prompt conditioning"},
),
"controlnets": (
[
"None",
"XLabs-AI/flux-controlnet-depth-v3",
"Shakker-Labs/FLUX.1-dev-ControlNet-Depth",
"jasperai/Flux.1-dev-Controlnet-Depth",
"jasperai/Flux.1-dev-Controlnet-Surface-Normals",
"XLabs-AI/flux-controlnet-canny-v3",
"InstantX/FLUX.1-dev-Controlnet-Canny",
"jasperai/Flux.1-dev-Controlnet-Upscaler",
"promeai/FLUX.1-controlnet-lineart-promeai",
],
{"default": "None", "tooltip": "ControlNet model used to guide generation"},
),
"controlnet_unions": (
[
"None",
"Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro",
"InstantX/FLUX.1-dev-Controlnet-Union",
],
{"default": "None", "tooltip": "ControlNet Union model used to guide generation"},
),
"controlnet_union_control_mode": (
["canny", "tile", "depth", "blur", "pose", "gray", "low_quality"],
{"default": "canny", "tooltip": "Control mode for the ControlNet Union model (canny, depth, pose, ...)"},
),
"control_image": ("IMAGE", {"tooltip": "Control image for the selected ControlNet"}),
"control_mask": ("MASK", {"tooltip": "Mask limiting where the ControlNet conditioning is applied"}),
"ip_adapter_image": ("IMAGE", {"tooltip": "Reference image for the IP-Adapter"}),
"ip_adapter_mask": ("MASK", {"tooltip": "Mask limiting where the IP-Adapter conditioning is applied"}),
"lora_path_1": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #1 to apply"}),
"lora_scale_1": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #1 (1.0 = full effect)"},
),
"lora_path_2": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #2 to apply"}),
"lora_scale_2": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #2 (1.0 = full effect)"},
),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
real_cfg_scale,
num_images,
enable_safety_checker,
use_real_cfg,
sync_mode,
seed=-1,
lora_path_1="",
lora_scale_1=1.0,
lora_path_2="",
lora_scale_2=1.0,
ip_adapter_scale=0.6,
controlnet_conditioning_scale=0.6,
controlnet_union_control_mode="canny",
ip_adapters="None",
controlnets="None",
controlnet_unions="None",
control_image=None,
control_mask=None,
ip_adapter_image=None,
ip_adapter_mask=None,
):
arguments = {
"prompt": prompt,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"real_cfg_scale": real_cfg_scale,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
"use_real_cfg": use_real_cfg,
"sync_mode": sync_mode,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
# Add ip_adapters if selected
if ip_adapters != "None":
arguments["ip_adapters"] = [
{
"path": ip_adapters,
"image_encoder_path": "openai/clip-vit-large-patch14",
"scale": ip_adapter_scale,
}
]
# Controlnet mapping
controlnet_mapping = {
"XLabs-AI/flux-controlnet-depth-v3": "https://huggingface.co/XLabs-AI/flux-controlnet-depth-v3/resolve/main/flux-depth-controlnet-v3.safetensors",
"Shakker-Labs/FLUX.1-dev-ControlNet-Depth": "https://huggingface.co/Shakker-Labs/FLUX.1-dev-ControlNet-Depth/resolve/main/diffusion_pytorch_model.safetensors",
"jasperai/Flux.1-dev-Controlnet-Depth": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Depth/resolve/main/diffusion_pytorch_model.safetensors",
"jasperai/Flux.1-dev-Controlnet-Surface-Normals": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Surface-Normals/resolve/main/diffusion_pytorch_model.safetensors",
"XLabs-AI/flux-controlnet-canny-v3": "https://huggingface.co/XLabs-AI/flux-controlnet-canny-v3/resolve/main/flux-canny-controlnet-v3.safetensors",
"InstantX/FLUX.1-dev-Controlnet-Canny": "https://huggingface.co/InstantX/FLUX.1-dev-Controlnet-Canny/resolve/main/diffusion_pytorch_model.safetensors",
"jasperai/Flux.1-dev-Controlnet-Upscaler": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Upscaler/resolve/main/diffusion_pytorch_model.safetensors",
"promeai/FLUX.1-controlnet-lineart-promeai": "https://huggingface.co/promeai/FLUX.1-controlnet-lineart-promeai/resolve/main/diffusion_pytorch_model.safetensors",
}
# Add controlnets if selected
if controlnets != "None":
controlnet_path = controlnet_mapping.get(controlnets, controlnets)
arguments["controlnets"] = [
{
"path": controlnet_path,
"conditioning_scale": controlnet_conditioning_scale,
}
]
# Add controlnet_unions if selected
if controlnet_unions != "None":
arguments["controlnet_unions"] = [
{
"path": controlnet_unions,
"controls": [
{
"control_mode": controlnet_union_control_mode,
}
],
}
]
# Handle controlnets
if controlnets != "None" and control_image is not None:
control_image_url = ImageUtils.upload_image(control_image)
if control_image_url:
controlnet_path = controlnet_mapping.get(controlnets, controlnets)
arguments["controlnets"] = [
{
"path": controlnet_path,
"conditioning_scale": controlnet_conditioning_scale,
"control_image_url": control_image_url,
}
]
if control_mask is not None:
mask_image = ImageUtils.mask_to_image(control_mask)
mask_image_url = ImageUtils.upload_image(mask_image)
if mask_image_url:
arguments["controlnets"][0]["mask_image_url"] = mask_image_url
# Handle controlnet_unions
if controlnet_unions != "None" and control_image is not None:
control_image_url = ImageUtils.upload_image(control_image)
if control_image_url:
arguments["controlnet_unions"] = [
{
"path": controlnet_unions,
"controls": [
{
"control_mode": controlnet_union_control_mode,
"control_image_url": control_image_url,
}
],
}
]
if control_mask is not None:
mask_image = ImageUtils.mask_to_image(control_mask)
mask_image_url = ImageUtils.upload_image(mask_image)
if mask_image_url:
arguments["controlnet_unions"][0]["controls"][0][
"mask_image_url"
] = mask_image_url
# Handle ip_adapters
if ip_adapters != "None" and ip_adapter_image is not None:
ip_adapter_image_url = ImageUtils.upload_image(ip_adapter_image)
if ip_adapter_image_url:
ip_adapter_path = (
"https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/flux-ip-adapter.safetensors?download=true"
if ip_adapters == "XLabs-AI/flux-ip-adapter"
else ip_adapters
)
arguments["ip_adapters"] = [
{
"path": ip_adapter_path,
"image_encoder_path": "openai/clip-vit-large-patch14",
"image_url": ip_adapter_image_url,
"scale": ip_adapter_scale,
}
]
if ip_adapter_mask is not None:
mask_image = ImageUtils.mask_to_image(ip_adapter_mask)
mask_image_url = ImageUtils.upload_image(mask_image)
if mask_image_url:
arguments["ip_adapters"][0]["mask_image_url"] = mask_image_url
# Add LoRAs if provided
loras = []
if lora_path_1:
loras.append({"path": lora_path_1, "scale": lora_scale_1})
if lora_path_2:
loras.append({"path": lora_path_2, "scale": lora_scale_2})
if loras:
arguments["loras"] = loras
try:
result = ApiHandler.submit_and_get_result("fal-ai/flux-general", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("FluxGeneral", e)
class FluxProKontext:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image": ("IMAGE", {"tooltip": "Input image to edit or transform"}),
},
"optional": {
"aspect_ratio": (
[
None,
"21:9",
"16:9",
"4:3",
"3:2",
"1:1",
"2:3",
"3:4",
"9:16",
"9:21",
],
{"default": None, "tooltip": "Aspect ratio of the generated image"},
),
"max_quality": ("BOOLEAN", {"default": False, "tooltip": "Use the higher-quality 'Max' endpoint (better results, higher cost)"}),
"guidance_scale": (
"FLOAT",
{"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
"output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 2**32 - 1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image,
aspect_ratio="1:1",
max_quality=False,
guidance_scale=3.5,
num_images=1,
safety_tolerance="2",
output_format="jpeg",
sync_mode=False,
seed=0,
):
# Upload the input image to get URL (raises on failure)
image_url = ImageUtils.upload_image(image)
# Dynamic endpoint selection based on max_quality toggle
endpoint = (
"fal-ai/flux-pro/kontext/max" if max_quality else "fal-ai/flux-pro/kontext"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"aspect_ratio": aspect_ratio,
"guidance_scale": guidance_scale,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
"output_format": output_format,
"sync_mode": sync_mode,
}
if seed > 0:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
model_name = "Flux Pro Kontext Max" if max_quality else "Flux Pro Kontext"
return ApiHandler.handle_image_generation_error(model_name, e)
class FluxProKontextMulti:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_1": ("IMAGE", {"tooltip": "Input reference image 1"}),
"image_2": ("IMAGE", {"tooltip": "Input reference image 2"}),
},
"optional": {
"image_3": ("IMAGE", {"tooltip": "Input reference image 3"}),
"image_4": ("IMAGE", {"tooltip": "Input reference image 4"}),
"aspect_ratio": (
[
None,
"21:9",
"16:9",
"4:3",
"3:2",
"1:1",
"2:3",
"3:4",
"9:16",
"9:21",
],
{"default": None, "tooltip": "Aspect ratio of the generated image"},
),
"max_quality": ("BOOLEAN", {"default": False, "tooltip": "Use the higher-quality 'Max' endpoint (better results, higher cost)"}),
"guidance_scale": (
"FLOAT",
{"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
"output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 2**32 - 1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_1,
image_2,
image_3=None,
image_4=None,
aspect_ratio="1:1",
max_quality=False,
guidance_scale=3.5,
num_images=1,
safety_tolerance="2",
output_format="jpeg",
sync_mode=False,
seed=0,
):
# Upload all provided images (upload raises on failure)
image_urls = [
ImageUtils.upload_image(img)
for img in [image_1, image_2, image_3, image_4]
if img is not None
]
if len(image_urls) < 2:
model_name = (
"Flux Pro Kontext Max Multi"
if max_quality
else "Flux Pro Kontext Multi"
)
return ApiHandler.handle_image_generation_error(
model_name, "At least 2 images are required."
)
# Dynamic endpoint selection based on max_quality toggle
endpoint = (
"fal-ai/flux-pro/kontext/max/multi"
if max_quality
else "fal-ai/flux-pro/kontext/multi"
)
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"aspect_ratio": aspect_ratio,
"guidance_scale": guidance_scale,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
"output_format": output_format,
"sync_mode": sync_mode,
}
if seed > 0:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
model_name = (
"Flux Pro Kontext Max Multi"
if max_quality
else "Flux Pro Kontext Multi"
)
return ApiHandler.handle_image_generation_error(model_name, e)
class FluxProKontextTextToImage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"aspect_ratio": (
["21:9", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "9:21"],
{"default": "1:1", "tooltip": "Aspect ratio of the generated image"},
),
"max_quality": ("BOOLEAN", {"default": False, "tooltip": "Use the higher-quality 'Max' endpoint (better results, higher cost)"}),
"guidance_scale": (
"FLOAT",
{"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2", "tooltip": "Content moderation strictness (1 = strictest, 6 = most permissive)"}),
"output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 2**32 - 1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
aspect_ratio="1:1",
max_quality=False,
guidance_scale=3.5,
num_images=1,
safety_tolerance="2",
output_format="jpeg",
sync_mode=False,
seed=0,
):
# Dynamic endpoint selection based on max_quality toggle
endpoint = (
"fal-ai/flux-pro/kontext/max/text-to-image"
if max_quality
else "fal-ai/flux-pro/kontext/text-to-image"
)
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"guidance_scale": guidance_scale,
"num_images": num_images,
"safety_tolerance": safety_tolerance,
"output_format": output_format,
"sync_mode": sync_mode,
}
if seed > 0:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
model_name = (
"Flux Pro Kontext Max Text-to-Image"
if max_quality
else "Flux Pro Kontext Text-to-Image"
)
return ApiHandler.handle_image_generation_error(model_name, e)
class Imagen4PreviewNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
):
arguments = {
"prompt": prompt,
}
try:
result = ApiHandler.submit_and_get_result(
"fal-ai/imagen4/preview", arguments
)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Imagen4 Preview", e)
class QwenImageEdit:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image": ("IMAGE", {"tooltip": "Input image to edit or transform"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "square_hd", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": (
"INT",
{"default": 512, "min": 128, "max": 2048, "step": 8, "tooltip": "Output width in pixels (used when image_size is 'custom')"},
),
"height": (
"INT",
{"default": 512, "min": 128, "max": 2048, "step": 8, "tooltip": "Output height in pixels (used when image_size is 'custom')"},
),
"num_inference_steps": ("INT", {"default": 30, "min": 1, "max": 50, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": (
"FLOAT",
{"default": 4.0, "min": 1.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
"output_format": (["png", "jpeg"], {"default": "png", "tooltip": "File format of the generated image"}),
"acceleration": (["none", "regular", "high"], {"default": "none", "tooltip": "Inference acceleration level; higher is faster with a possible quality trade-off"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
"optional": {
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "What to avoid in the generated image"}),
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "edit_image"
CATEGORY = "FAL/Image"
def edit_image(
self,
prompt,
image,
image_size,
width,
height,
num_inference_steps,
guidance_scale,
num_images,
enable_safety_checker,
output_format,
acceleration,
sync_mode,
negative_prompt="",
seed=-1,
):
model_name = "Qwen Image Edit"
image_url = ImageUtils.upload_image(image)
arguments = {
"prompt": prompt,
"image_url": image_url,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
"output_format": output_format,
"acceleration": acceleration,
"sync_mode": sync_mode,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if negative_prompt:
arguments["negative_prompt"] = negative_prompt
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/qwen-image-edit", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error(model_name, e)
class QwenImageEditPlusLoRA:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Batched input images; every image in the batch is uploaded as a reference"})
},
"optional": {
"image_size": (
[
"custom",
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
],
{"default": "landscape_16_9", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"custom_width": ("INT", {"default": 1920, "min": 128, "max": 2048, "step": 8, "tooltip": "Output width in pixels (used when image_size is 'custom')"}),
"custom_height": ("INT", {"default": 1080, "min": 128, "max": 2048, "step": 8, "tooltip": "Output height in pixels (used when image_size is 'custom')"}),
"num_inference_steps": ("INT", {"default": 28, "min": 2, "max": 50, "tooltip": "Number of denoising steps; more steps can improve quality but take longer"}),
"guidance_scale": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 20.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"}),
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "What to avoid in the generated image"}),
"seed": ("INT", {"default": -1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
"output_format": (["png", "jpeg"], {"default": "png", "tooltip": "File format of the generated image"}),
"lora_path_1": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #1 to apply"}),
"lora_scale_1": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #1 (1.0 = full effect)"},
),
"lora_path_2": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #2 to apply"}),
"lora_scale_2": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #2 (1.0 = full effect)"},
),
"lora_path_3": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #3 to apply"}),
"lora_scale_3": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #3 (1.0 = full effect)"},
),
"lora_path_4": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights file #4 to apply"}),
"lora_scale_4": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "Strength of LoRA #4 (1.0 = full effect)"},
),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "edit_image"
CATEGORY = "FAL/Image"
def edit_image(
self,
prompt,
images=None,
image_size="square_hd",
custom_width=1024,
custom_height=1024,
num_inference_steps=28,
guidance_scale=4.0,
negative_prompt="",
seed=-1,
num_images=1,
enable_safety_checker=True,
output_format="png",
lora_path_1="",
lora_scale_1=1.0,
lora_path_2="",
lora_scale_2=1.0,
lora_path_3="",
lora_scale_3=1.0,
lora_path_4="",
lora_scale_4=1.0,
):
model_name = "Qwen Image Edit Plus LoRA"
# Handle multiple images
image_urls = ImageUtils.prepare_images(images)
if not image_urls:
return ApiHandler.handle_image_generation_error(
model_name, "No images provided."
)
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"num_images": num_images,
"enable_safety_checker": enable_safety_checker,
"output_format": output_format,
}
if image_size == "custom":
arguments["image_size"] = {"width": custom_width, "height": custom_height}
else:
arguments["image_size"] = image_size
if negative_prompt:
arguments["negative_prompt"] = negative_prompt
if seed != -1:
arguments["seed"] = seed
# Add LoRAs if provided (maximum 4)
loras = []
if lora_path_1:
loras.append({"path": lora_path_1, "scale": lora_scale_1})
if lora_path_2:
loras.append({"path": lora_path_2, "scale": lora_scale_2})
if lora_path_3:
loras.append({"path": lora_path_3, "scale": lora_scale_3})
if lora_path_4:
loras.append({"path": lora_path_4, "scale": lora_scale_4})
if loras:
arguments["loras"] = loras
try:
result = ApiHandler.submit_and_get_result(
"fal-ai/qwen-image-edit-plus-lora", arguments
)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error(model_name, e)
# https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=fal-ai/bytedance/seededit/v3/edit-image
class SeedEditV3:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image": ("IMAGE", {"tooltip": "Input image to edit or transform"}),
},
"optional": {
"guidance_scale": (
"FLOAT",
{"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "CFG scale: how closely the output follows the prompt (higher = stricter)"},
),
"seed": ("INT", {"default": -1, "min": -1, "max": 2**32 - 1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image,
guidance_scale=0.5,
seed=-1,
):
model_name = "SeedEdit 3.0"
image_url = ImageUtils.upload_image(image)
endpoint = "fal-ai/bytedance/seededit/v3/edit-image"
arguments = {
"prompt": prompt,
"image_url": image_url,
"guidance_scale": guidance_scale,
}
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_single_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error(model_name, e)
# https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=fal-ai/bytedance/seedream/v4/edit
class SeedreamV4Edit:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_1": ("IMAGE", {"tooltip": "Input reference image 1"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "square_hd", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": ("INT", {"default": 3840, "min": 1024, "max": 4096, "tooltip": "Output width in pixels (used when image_size is 'custom')"}),
"height": ("INT", {"default": 2160, "min": 1024, "max": 4096, "tooltip": "Output height in pixels (used when image_size is 'custom')"}),
},
"optional": {
"image_2": ("IMAGE", {"tooltip": "Input reference image 2"}),
"image_3": ("IMAGE", {"tooltip": "Input reference image 3"}),
"image_4": ("IMAGE", {"tooltip": "Input reference image 4"}),
"image_5": ("IMAGE", {"tooltip": "Input reference image 5"}),
"image_6": ("IMAGE", {"tooltip": "Input reference image 6"}),
"image_7": ("IMAGE", {"tooltip": "Input reference image 7"}),
"image_8": ("IMAGE", {"tooltip": "Input reference image 8"}),
"image_9": ("IMAGE", {"tooltip": "Input reference image 9"}),
"image_10": ("IMAGE", {"tooltip": "Input reference image 10"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 6, "tooltip": "Number of images to generate per run"}),
"max_images": ("INT", {"default": 1, "min": 1, "max": 6, "tooltip": "Maximum images returned per generation; total inputs + outputs must not exceed 15"}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the safety checker on generated images"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2**32 - 1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_1,
image_size,
width,
height,
num_images,
max_images,
image_2=None,
image_3=None,
image_4=None,
image_5=None,
image_6=None,
image_7=None,
image_8=None,
image_9=None,
image_10=None,
enable_safety_checker=True,
seed=-1,
sync_mode=False,
):
model_name = "Seedream 4.0 Edit"
# Upload all provided images (upload raises on failure)
image_urls = [
ImageUtils.upload_image(img)
for img in [
image_1,
image_2,
image_3,
image_4,
image_5,
image_6,
image_7,
image_8,
image_9,
image_10,
]
if img is not None
]
# the total number of images (image inputs + image outputs) must not exceed 15
max_total_images = 15
potential_total = len(image_urls) + (num_images * max_images)
if potential_total > max_total_images:
return ApiHandler.handle_image_generation_error(
model_name,
f"Total images (inputs + outputs) must be <= {max_total_images}. "
f"inputs={len(image_urls)}, num_images={num_images}, max_images={max_images}",
)
endpoint = "fal-ai/bytedance/seedream/v4/edit"
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"num_images": num_images,
"max_images": max_images,
"enable_safety_checker": enable_safety_checker,
"sync_mode": sync_mode,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error(model_name, e)
class NanoBananaTextToImage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"aspect_ratio": (
["21:9", "1:1", "4:3", "3:2", "2:3", "5:4", "4:5", "3:4", "16:9", "9:16"],
{"default": "1:1", "tooltip": "Aspect ratio of the generated image"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png"], {"default": "png", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
aspect_ratio="1:1",
num_images=1,
output_format="png",
sync_mode=False,
):
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"num_images": num_images,
"output_format": output_format,
"sync_mode": sync_mode,
}
try:
result = ApiHandler.submit_and_get_result("fal-ai/nano-banana", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Nano Banana Text-to-Image", e)
class NanoBananaEdit:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_1": ("IMAGE", {"tooltip": "Input reference image 1"}),
},
"optional": {
"image_2": ("IMAGE", {"tooltip": "Input reference image 2"}),
"image_3": ("IMAGE", {"tooltip": "Input reference image 3"}),
"image_4": ("IMAGE", {"tooltip": "Input reference image 4"}),
"images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Batched input images; every image in the batch is uploaded as a reference"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "File format of the generated image"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_1,
image_2=None,
image_3=None,
image_4=None,
images=None,
num_images=1,
output_format="jpeg",
):
# Upload all provided images
singleImages = ImageUtils.prepare_images([image_1, image_2, image_3, image_4])
batchImages = ImageUtils.prepare_images(images)
image_urls = singleImages + batchImages
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"num_images": num_images,
"output_format": output_format,
}
try:
result = ApiHandler.submit_and_get_result("fal-ai/nano-banana/edit", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Nano Banana Edit", e)
class NanoBananaPro:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"images": ("IMAGE", {"tooltip": "Batched input images; every image in the batch is uploaded as a reference"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"aspect_ratio": (
["auto", "21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16"],
{"default": "1:1", "tooltip": "Aspect ratio of the generated image"},
),
"output_format": (["jpeg", "png", "webp"], {"default": "png", "tooltip": "File format of the generated image"}),
"resolution": (["1K", "2K", "4K"], {"default": "1K", "tooltip": "Output resolution tier of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
images=None,
num_images=1,
aspect_ratio="1:1",
output_format="png",
resolution="1K",
sync_mode=False,
):
# Prepare image URLs from optional input, limit to 14 images max
if images is not None and hasattr(images, 'shape') and len(images.shape) == 4 and images.shape[0] > 14:
# If batch has more than 14 images, take only first 14
logger.warning(
"Nano Banana Pro: more than 14 input images provided; using the first 14"
)
images = images[:14]
image_urls = ImageUtils.prepare_images(images)
# Build base arguments
arguments = {
"prompt": prompt,
"num_images": num_images,
"aspect_ratio": aspect_ratio,
"output_format": output_format,
"resolution": resolution,
"sync_mode": sync_mode,
}
# Conditional endpoint routing based on whether ANY images provided
if len(image_urls) > 0:
# Use edit endpoint with image_urls array
endpoint = "fal-ai/nano-banana-pro/edit"
arguments["image_urls"] = image_urls
else:
# Use text-to-image endpoint (no image_urls parameter)
endpoint = "fal-ai/nano-banana-pro"
# Remove "auto" from aspect_ratio for text-to-image endpoint
if aspect_ratio == "auto":
arguments["aspect_ratio"] = "1:1"
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Nano Banana Pro", e)
class NanoBanana2:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"images": ("IMAGE", {"tooltip": "Batched input images; every image in the batch is uploaded as a reference"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"aspect_ratio": (
["auto", "21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16"],
{"default": "1:1", "tooltip": "Aspect ratio of the generated image"},
),
"output_format": (["jpeg", "png", "webp"], {"default": "png", "tooltip": "File format of the generated image"}),
"resolution": (["0.5K", "1K", "2K", "4K"], {"default": "1K", "tooltip": "Output resolution tier of the generated image"}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"enable_web_search": ("BOOLEAN", {"default": False, "tooltip": "Allow the model to use web search for factual or current context"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
images=None,
num_images=1,
aspect_ratio="1:1",
output_format="png",
resolution="1K",
seed=-1,
enable_web_search=False,
sync_mode=False,
):
# Prepare image URLs from optional input, limit to 14 images max
if images is not None and hasattr(images, 'shape') and len(images.shape) == 4 and images.shape[0] > 14:
logger.warning(
"Nano Banana 2: more than 14 input images provided; using the first 14"
)
images = images[:14]
image_urls = ImageUtils.prepare_images(images)
# Build base arguments
arguments = {
"prompt": prompt,
"num_images": num_images,
"aspect_ratio": aspect_ratio,
"output_format": output_format,
"resolution": resolution,
"enable_web_search": enable_web_search,
"sync_mode": sync_mode,
}
# Add seed if specified
if seed != -1:
arguments["seed"] = seed
# Conditional endpoint routing based on whether images provided
if len(image_urls) > 0:
endpoint = "fal-ai/nano-banana-2/edit"
arguments["image_urls"] = image_urls
else:
endpoint = "fal-ai/nano-banana-2"
# Remove "auto" from aspect_ratio for text-to-image endpoint
if aspect_ratio == "auto":
arguments["aspect_ratio"] = "1:1"
try:
result = ApiHandler.submit_and_get_result(endpoint, arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Nano Banana 2", e)
class ReveTextToImage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"aspect_ratio": (
["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"],
{"default": "3:2", "tooltip": "Aspect ratio of the generated image"},
),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png"], {"default": "png", "tooltip": "File format of the generated image"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
aspect_ratio="1:1",
num_images=1,
output_format="png",
):
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"num_images": num_images,
"output_format": output_format,
}
try:
result = ApiHandler.submit_and_get_result("fal-ai/reve/text-to-image", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Reve Text-to-Image", e)
class Dreamina31TextToImage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "square_hd", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
},
"optional": {
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
"output_format": (["jpeg", "png"], {"default": "png", "tooltip": "File format of the generated image"}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2**32 - 1, "tooltip": "Random seed for reproducible results; leave at the default for a random seed"}),
"enhance_prompt": ("BOOLEAN", {"default": False, "tooltip": "Let the model automatically enhance and expand the prompt"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size,
num_images=1,
sync_mode=False,
output_format="png",
seed=-1,
enhance_prompt=False,
):
arguments = {
"prompt": prompt,
"image_size": image_size,
"num_images": num_images,
"sync_mode": sync_mode,
"output_format": output_format,
"enhance_prompt": enhance_prompt,
}
if seed > 0:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/bytedance/dreamina/v3.1/text-to-image", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("Dreamina v3.1 Text-to-Image", e)
class GPTImage15Edit:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"images": ("IMAGE", {"tooltip": "Batched input images; every image in the batch is uploaded as a reference"}),
},
"optional": {
"mask_image": ("IMAGE", {"tooltip": "Inpainting mask: white areas are regenerated, black areas are preserved"}),
"image_size": (["auto", "1024x1024", "1536x1024", "1024x1536"], {"default": "auto", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"}),
"background": (["auto", "transparent", "opaque"], {"default": "auto", "tooltip": "Background transparency of the generated image (transparent requires png/webp)"}),
"quality": (["low", "medium", "high"], {"default": "high", "tooltip": "Rendering quality; higher quality costs more and takes longer"}),
"input_fidelity": (["low", "high"], {"default": "high", "tooltip": "How closely the output preserves the input image(s)"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png", "webp"], {"default": "png", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "edit_image"
CATEGORY = "FAL/Image"
def edit_image(
self,
prompt,
images,
mask_image=None,
image_size="auto",
background="auto",
quality="high",
input_fidelity="high",
num_images=1,
output_format="png",
sync_mode=False,
):
model_name = "GPT-Image 1.5"
# Prepare image URLs from input, limit to 16 images max
if images is not None and hasattr(images, 'shape') and len(images.shape) == 4 and images.shape[0] > 16:
# If batch has more than 16 images, take only first 16
logger.warning(
f"{model_name}: more than 16 input images provided; using the first 16"
)
images = images[:16]
image_urls = ImageUtils.prepare_images(images)
if len(image_urls) == 0:
return ApiHandler.handle_image_generation_error(
model_name, "No valid images provided."
)
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"image_size": image_size,
"background": background,
"quality": quality,
"input_fidelity": input_fidelity,
"num_images": num_images,
"output_format": output_format,
"sync_mode": sync_mode,
}
# Add optional mask image
if mask_image is not None:
arguments["mask_image_url"] = ImageUtils.upload_image(mask_image)
try:
result = ApiHandler.submit_and_get_result("fal-ai/gpt-image-1.5/edit", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error(model_name, e)
class GPTImage15:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"image_size": (["1024x1024", "1536x1024", "1024x1536"], {"default": "1024x1024", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"}),
"background": (["auto", "transparent", "opaque"], {"default": "auto", "tooltip": "Background transparency of the generated image (transparent requires png/webp)"}),
"quality": (["low", "medium", "high"], {"default": "high", "tooltip": "Rendering quality; higher quality costs more and takes longer"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png", "webp"], {"default": "png", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size="1024x1024",
background="auto",
quality="high",
num_images=1,
output_format="png",
sync_mode=False,
):
arguments = {
"prompt": prompt,
"image_size": image_size,
"background": background,
"quality": quality,
"num_images": num_images,
"output_format": output_format,
"sync_mode": sync_mode,
}
try:
result = ApiHandler.submit_and_get_result("fal-ai/gpt-image-1.5", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("GPT-Image 1.5", e)
class GPTImage2Edit:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
"image_1": ("IMAGE", {"tooltip": "Input reference image 1"}),
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "custom", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": ("INT", {"default": 3840, "min": 768, "max": 4096, "tooltip": "Output width in pixels (used when image_size is 'custom')"}),
"height": ("INT", {"default": 2160, "min": 768, "max": 4096, "tooltip": "Output height in pixels (used when image_size is 'custom')"}),
},
"optional": {
"image_2": ("IMAGE", {"tooltip": "Input reference image 2"}),
"image_3": ("IMAGE", {"tooltip": "Input reference image 3"}),
"image_4": ("IMAGE", {"tooltip": "Input reference image 4"}),
"image_5": ("IMAGE", {"tooltip": "Input reference image 5"}),
"image_6": ("IMAGE", {"tooltip": "Input reference image 6"}),
"image_7": ("IMAGE", {"tooltip": "Input reference image 7"}),
"image_8": ("IMAGE", {"tooltip": "Input reference image 8"}),
"image_9": ("IMAGE", {"tooltip": "Input reference image 9"}),
"image_10": ("IMAGE", {"tooltip": "Input reference image 10"}),
"image_11": ("IMAGE", {"tooltip": "Input reference image 11"}),
"image_12": ("IMAGE", {"tooltip": "Input reference image 12"}),
"image_13": ("IMAGE", {"tooltip": "Input reference image 13"}),
"image_14": ("IMAGE", {"tooltip": "Input reference image 14"}),
"image_15": ("IMAGE", {"tooltip": "Input reference image 15"}),
"image_16": ("IMAGE", {"tooltip": "Input reference image 16"}),
"mask_image": ("IMAGE", {"tooltip": "Inpainting mask: white areas are regenerated, black areas are preserved"}),
"background": (["auto", "transparent", "opaque"], {"default": "opaque", "tooltip": "Background transparency of the generated image (transparent requires png/webp)"}),
"quality": (["low", "medium", "high"], {"default": "high", "tooltip": "Rendering quality; higher quality costs more and takes longer"}),
"input_fidelity": (["low", "high"], {"default": "high", "tooltip": "How closely the output preserves the input image(s)"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png", "webp"], {"default": "png", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "edit_image"
CATEGORY = "FAL/Image"
def edit_image(
self,
prompt,
image_1,
image_size,
width,
height,
image_2=None,
image_3=None,
image_4=None,
image_5=None,
image_6=None,
image_7=None,
image_8=None,
image_9=None,
image_10=None,
image_11=None,
image_12=None,
image_13=None,
image_14=None,
image_15=None,
image_16=None,
mask_image=None,
background="opaque",
quality="high",
input_fidelity="high",
num_images=1,
output_format="png",
sync_mode=False,
):
model_name = "GPT-Image 2 Edit"
# Upload all provided images (upload raises on failure)
image_urls = [
ImageUtils.upload_image(img)
for img in [
image_1,
image_2,
image_3,
image_4,
image_5,
image_6,
image_7,
image_8,
image_9,
image_10,
image_11,
image_12,
image_13,
image_14,
image_15,
image_16,
]
if img is not None
]
if len(image_urls) == 0:
return ApiHandler.handle_image_generation_error(
model_name, "No valid images provided."
)
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"background": background,
"quality": quality,
"input_fidelity": input_fidelity,
"num_images": num_images,
"output_format": output_format,
"sync_mode": sync_mode,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
if mask_image is not None:
arguments["mask_image_url"] = ImageUtils.upload_image(mask_image)
try:
result = ApiHandler.submit_and_get_result("fal-ai/gpt-image-2/edit", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error(model_name, e)
class GPTImage2:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the image to generate or the edit to apply"}),
},
"optional": {
"image_size": (
[
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
"custom",
],
{"default": "custom", "tooltip": "Preset output size/aspect ratio; select 'custom' to use width and height"},
),
"width": ("INT", {"default": 3840, "min": 768, "max": 4096, "tooltip": "Output width in pixels (used when image_size is 'custom')"}),
"height": ("INT", {"default": 2160, "min": 768, "max": 4096, "tooltip": "Output height in pixels (used when image_size is 'custom')"}),
"background": (["auto", "transparent", "opaque"], {"default": "opaque", "tooltip": "Background transparency of the generated image (transparent requires png/webp)"}),
"quality": (["low", "medium", "high"], {"default": "high", "tooltip": "Rendering quality; higher quality costs more and takes longer"}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "Number of images to generate per run"}),
"output_format": (["jpeg", "png", "webp"], {"default": "png", "tooltip": "File format of the generated image"}),
"sync_mode": ("BOOLEAN", {"default": False, "tooltip": "Wait for generation and return the image data directly instead of a hosted URL"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_image"
CATEGORY = "FAL/Image"
def generate_image(
self,
prompt,
image_size="custom",
width=3840,
height=2160,
background="auto",
quality="high",
num_images=1,
output_format="png",
sync_mode=False,
):
arguments = {
"prompt": prompt,
"background": background,
"quality": quality,
"num_images": num_images,
"output_format": output_format,
"sync_mode": sync_mode,
}
if image_size == "custom":
arguments["image_size"] = {"width": width, "height": height}
else:
arguments["image_size"] = image_size
try:
result = ApiHandler.submit_and_get_result("fal-ai/gpt-image-2", arguments)
return ResultProcessor.process_image_result(result)
except Exception as e:
return ApiHandler.handle_image_generation_error("GPT-Image 2", e)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"Ideogramv3_fal": Ideogramv3,
"Hidreamfull_fal": HidreamFull,
"FluxPro_fal": FluxPro,
"FluxDev_fal": FluxDev,
"FluxSchnell_fal": FluxSchnell,
"FluxPro11_fal": FluxPro11,
"FluxPro1Fill_fal": FluxPro1Fill,
"FluxUltra_fal": FluxUltra,
"FluxGeneral_fal": FluxGeneral,
"FluxLora_fal": FluxLora,
"Recraft_fal": Recraft,
"Sana_fal": Sana,
"FluxProKontext_fal": FluxProKontext,
"FluxProKontextMulti_fal": FluxProKontextMulti,
"FluxProKontextTextToImage_fal": FluxProKontextTextToImage,
"Imagen4Preview_fal": Imagen4PreviewNode,
"QwenImageEdit_fal": QwenImageEdit,
"QwenImageEditPlusLoRA_fal": QwenImageEditPlusLoRA,
"SeedEditV3_fal": SeedEditV3,
"SeedreamV4Edit_fal": SeedreamV4Edit,
"NanoBananaTextToImage_fal": NanoBananaTextToImage,
"NanoBananaEdit_fal": NanoBananaEdit,
"NanoBananaPro_fal": NanoBananaPro,
"NanoBanana2_fal": NanoBanana2,
"ReveTextToImage_fal": ReveTextToImage,
"Dreamina31TextToImage_fal": Dreamina31TextToImage,
"GPTImage15Edit_fal": GPTImage15Edit,
"GPTImage15_fal": GPTImage15,
"GPTImage2Edit_fal": GPTImage2Edit,
"GPTImage2_fal": GPTImage2,
}
# Display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"Ideogramv3_fal": "Ideogramv3 (fal)",
"Hidreamfull_fal": "HidreamFull (fal)",
"FluxPro_fal": "Flux Pro (fal)",
"FluxDev_fal": "Flux Dev (fal)",
"FluxSchnell_fal": "Flux Schnell (fal)",
"FluxPro11_fal": "Flux Pro 1.1 (fal)",
"FluxPro1Fill_fal": "Flux Pro 1 Fill (fal)",
"FluxUltra_fal": "Flux Ultra (fal)",
"FluxGeneral_fal": "Flux General (fal)",
"FluxLora_fal": "Flux LoRA (fal)",
"Recraft_fal": "Recraft V3 (fal)",
"Sana_fal": "Sana (fal)",
"FluxProKontext_fal": "Flux Pro Kontext (fal)",
"FluxProKontextMulti_fal": "Flux Pro Kontext Multi (fal)",
"FluxProKontextTextToImage_fal": "Flux Pro Kontext Text-to-Image (fal)",
"Imagen4Preview_fal": "Imagen4 Preview (fal)",
"QwenImageEdit_fal": "Qwen Image Edit (fal)",
"QwenImageEditPlusLoRA_fal": "Qwen Image Edit Plus LoRA (fal)",
"SeedEditV3_fal": "SeedEdit 3.0 (fal)",
"SeedreamV4Edit_fal": "Seedream 4.0 Edit (fal)",
"NanoBananaTextToImage_fal": "Nano Banana Text-to-Image (fal)",
"NanoBananaEdit_fal": "Nano Banana Edit (fal)",
"NanoBananaPro_fal": "Nano Banana Pro (fal)",
"NanoBanana2_fal": "Nano Banana 2 (fal)",
"ReveTextToImage_fal": "Reve Text-to-Image (fal)",
"Dreamina31TextToImage_fal": "Dreamina v3.1 Text-to-Image (fal)",
"GPTImage15Edit_fal": "GPT-Image 1.5 Edit (fal)",
"GPTImage15_fal": "GPT-Image 1.5 (fal)",
"GPTImage2Edit_fal": "GPT-Image 2 Edit (fal)",
"GPTImage2_fal": "GPT-Image 2.0 (fal)",
}