flux2turbo

This commit is contained in:
nofunstudio
2026-01-04 14:02:38 -07:00
parent d6d14163fc
commit a5fe9f6a0d
3 changed files with 281 additions and 7 deletions
+3
View File
@@ -12,6 +12,7 @@ from .fal_seedance_video import FalAPI_seedance_video
from .nano_banana_pro import FalAPI_NanoBananaPro
from .fal_qwen_edit import FalAPI_QwenEditPlus
from .fal_flux2_lora_edit import FalAPI_Flux2LoraEdit
from .fal_flux2_turbo_edit import FalAPI_Flux2TurboEdit
from .wavespeed_flux2_lora_edit import WaveSpeedAI_Flux2LoraEdit
from .wavespeed_ai_image import WaveSpeedAI_Image
from .string_lower import StringLower
@@ -34,6 +35,7 @@ NODE_CLASS_MAPPINGS = {
"FalAPI_NanoBananaPro": FalAPI_NanoBananaPro,
"FalAPI_QwenEditPlus": FalAPI_QwenEditPlus,
"FalAPI_Flux2LoraEdit": FalAPI_Flux2LoraEdit,
"FalAPI_Flux2TurboEdit": FalAPI_Flux2TurboEdit,
"WaveSpeedAI_Flux2LoraEdit": WaveSpeedAI_Flux2LoraEdit,
"WaveSpeedAI_Image": WaveSpeedAI_Image,
"StringLower": StringLower,
@@ -55,6 +57,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FalAPI_NanoBananaPro": "Nano Banana Pro (FAL)",
"FalAPI_QwenEditPlus": "FAL Qwen Edit Plus",
"FalAPI_Flux2LoraEdit": "FAL Flux 2 LoRA Edit",
"FalAPI_Flux2TurboEdit": "FAL Flux 2 Turbo Edit",
"WaveSpeedAI_Flux2LoraEdit": "WaveSpeedAI Flux 2 LoRA Edit",
"WaveSpeedAI_Image": "WaveSpeedAI Image Generation",
"StringLower": "String to Lowercase",
+265
View File
@@ -0,0 +1,265 @@
import os
import time
import requests
import tempfile
import json
import random
import numpy as np
import torch
import asyncio
import folder_paths
import fal_client
from io import BytesIO
from PIL import Image
import base64
class FalAPI_Flux2TurboEdit:
@classmethod
def INPUT_TYPES(cls):
"""
An image generation node for `fal-ai/flux-2/turbo/edit`.
"""
return {
"required": {
"api_key": ("STRING", {
"multiline": False,
"default": os.getenv("FAL_KEY", ""),
"display": "FAL API Key"
}),
"prompt": ("STRING", {
"multiline": True,
"default": "Make this image more vibrant and detailed",
"display": "Prompt"
}),
"image1": ("IMAGE", {"display": "Image 1"}),
"width": ("INT", {"default": 1024, "min": 512, "max": 2048, "step": 8, "display": "Width"}),
"height": ("INT", {"default": 1024, "min": 512, "max": 2048, "step": 8, "display": "Height"}),
},
"optional": {
"image2": ("IMAGE", {"display": "Image 2 (optional)"}),
"image3": ("IMAGE", {"display": "Image 3 (optional)"}),
"guidance_scale": ("FLOAT", {
"default": 2.5,
"min": 0.0,
"max": 20.0,
"step": 0.1,
"display": "Guidance Scale"
}),
"seed": ("INT", {
"default": -1,
"min": -1,
"max": 2147483647,
"display": "Seed (-1 for random)"
}),
"output_format": (["jpeg", "png", "webp"], {
"default": "png",
"display": "Output Format"
}),
"enable_safety_checker": ("BOOLEAN", {
"default": True,
"display": "Enable Safety Checker"
}),
}
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING",)
RETURN_NAMES = ("image", "image_path", "generation_info",)
FUNCTION = "generate_image"
CATEGORY = "FAL"
def __init__(self):
self.temp_dir = folder_paths.get_temp_directory()
self.output_dir = os.path.join("output", "API", "FAL", "flux-2-turbo-edit")
self.metadata_dir = os.path.join(self.output_dir, "metadata")
os.makedirs(self.output_dir, exist_ok=True)
os.makedirs(self.metadata_dir, exist_ok=True)
def get_next_number(self):
valid_exts = {".png", ".jpg", ".webp", ".jpeg"}
files = [f for f in os.listdir(self.output_dir)
if os.path.splitext(f)[1].lower() in valid_exts]
numbers = []
for file_name in files:
base, _ = os.path.splitext(file_name)
try:
numbers.append(int(base))
except ValueError:
pass
return max(numbers) + 1 if numbers else 1
def save_image_and_metadata(self, pil_img, generation_info, number, ext="png"):
filename = f"{number:03d}.{ext}"
filepath = os.path.join(self.output_dir, filename)
pil_img.save(filepath)
metadata_filename = f"{number:03d}_metadata.json"
metadata_filepath = os.path.join(self.metadata_dir, metadata_filename)
with open(metadata_filepath, "w", encoding="utf-8") as f:
json.dump(generation_info, f, indent=4, ensure_ascii=False)
return filepath, metadata_filepath
def on_queue_update(self, update):
try:
if isinstance(update, fal_client.InProgress):
for log in update.logs:
print(f"[Flux2TurboEdit] {log['message']}")
except Exception as e:
print(f"[Flux2TurboEdit] Error in queue update: {e}")
def tensor_to_tempfile(self, tensor):
pil_img = self.tensor_to_pil(tensor)
fd, filename = tempfile.mkstemp(suffix=".png")
os.close(fd)
pil_img.save(filename, format="PNG")
return filename
def tensor_to_pil(self, tensor):
if len(tensor.shape) == 4:
tensor = tensor[0]
arr = tensor.cpu().numpy()
if arr.ndim == 3 and arr.shape[0] <= 4:
arr = np.transpose(arr, (1, 2, 0))
arr = (arr * 255).clip(0, 255).astype("uint8")
return Image.fromarray(arr)
def pil_to_tensor(self, pil_img):
img_array = np.array(pil_img).astype(np.float32) / 255.0
return torch.from_numpy(img_array)[None,]
async def generate_image(self, api_key, prompt, image1, width, height,
image2=None, image3=None, guidance_scale=2.5,
seed=-1, output_format="png", enable_safety_checker=True):
print("[Flux2TurboEdit] Starting image generation process...")
if not api_key:
raise ValueError("A FAL API key is required.")
os.environ["FAL_KEY"] = api_key
temp_files = []
try:
image_urls = []
all_images = [image1, image2, image3]
# Upload images
for i, img_tensor in enumerate(all_images):
if img_tensor is not None:
print(f"[Flux2TurboEdit] Processing input image {i+1}...")
# Convert to temp file and upload
temp_path = await asyncio.to_thread(self.tensor_to_tempfile, img_tensor)
temp_files.append(temp_path)
print(f"[Flux2TurboEdit] Uploading image {i+1}...")
url = await asyncio.to_thread(fal_client.upload_file, temp_path)
image_urls.append(url)
if not image_urls:
raise ValueError("At least one image input is required.")
# Prepare arguments
arguments = {
"prompt": prompt,
"image_urls": image_urls,
"image_size": {"width": width, "height": height},
"guidance_scale": guidance_scale,
"enable_safety_checker": enable_safety_checker,
"output_format": output_format,
"num_images": 1
}
if seed != -1:
arguments["seed"] = seed
print(f"[Flux2TurboEdit] Arguments: {json.dumps(arguments, indent=2)}")
endpoint = "fal-ai/flux-2/turbo/edit"
print(f"[Flux2TurboEdit] Submitting to {endpoint}...")
# Call API
result = await asyncio.to_thread(
fal_client.subscribe,
endpoint,
arguments=arguments,
with_logs=True,
on_queue_update=self.on_queue_update,
)
# Process results
if not result:
raise RuntimeError("No result returned from API")
output_images = result.get("images", [])
if not output_images:
raise RuntimeError(f"No images in result: {result.keys()}")
print(f"[Flux2TurboEdit] Downloaded {len(output_images)} images.")
final_tensors = []
saved_paths = []
for img_info in output_images:
img_url = img_info.get("url")
if not img_url: continue
# Download
resp = await asyncio.to_thread(requests.get, img_url)
if resp.status_code == 200:
pil_img = Image.open(BytesIO(resp.content)).convert("RGB")
output_tensor = self.pil_to_tensor(pil_img)
final_tensors.append(output_tensor)
# Save
number = self.get_next_number()
gen_info = {
"prompt": prompt,
"parameters": arguments,
"result": result
}
path, _ = await asyncio.to_thread(self.save_image_and_metadata, pil_img, gen_info, number, output_format)
saved_paths.append(path)
if not final_tensors:
raise RuntimeError("Failed to process output images")
if len(final_tensors) > 1:
batch_tensor = torch.cat(final_tensors, dim=0)
else:
batch_tensor = final_tensors[0]
return (batch_tensor, ";".join(saved_paths), json.dumps(result, indent=2))
except Exception as e:
print(f"[Flux2TurboEdit] Error: {str(e)}")
# Clean up
for f in temp_files:
try: os.remove(f)
except: pass
error_info = {
"error": f"Flux2TurboEdit generation failed: {str(e)}",
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
}
return (torch.zeros((1, 64, 64, 3)), "", json.dumps(error_info, indent=2))
finally:
# Cleanup temp inputs
for f in temp_files:
try: os.remove(f)
except: pass
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
NODE_CLASS_MAPPINGS = {
"FalAPI_Flux2TurboEdit": FalAPI_Flux2TurboEdit
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FalAPI_Flux2TurboEdit": "FAL Flux 2 Turbo Edit"
}
__all__ = ["FalAPI_Flux2TurboEdit"]
+13 -7
View File
@@ -17,7 +17,7 @@ class FalAPI_seedance_video:
@classmethod
def INPUT_TYPES(cls):
"""
A video generation node for `fal-ai/bytedance/seedance/v1/pro/image-to-video`.
A video generation node for `fal-ai/bytedance/seedance/v1.5/pro/image-to-video`.
Takes an image input, prompt, and generates a video using Seedance Video API.
"""
return {
@@ -74,6 +74,10 @@ class FalAPI_seedance_video:
"default": True,
"display": "Camera Fixed"
}),
"generate_audio": ("BOOLEAN", {
"default": True,
"display": "Generate Audio"
}),
"enable_safety_checker": ("BOOLEAN", {
"default": True,
"display": "Enable Safety Checker"
@@ -139,9 +143,9 @@ class FalAPI_seedance_video:
print(f"[Seedance Video] Error in queue update: {e}")
async def generate_video(self, api_token, image, prompt, aspect_ratio, resolution,
duration, camera_fixed, enable_safety_checker, end_image=None, seed=-1):
duration, camera_fixed, generate_audio, enable_safety_checker, end_image=None, seed=-1):
"""
Generate video using fal-ai/bytedance/seedance/v1/pro/image-to-video
Generate video using fal-ai/bytedance/seedance/v1.5/pro/image-to-video
(Async execution for parallel processing)
"""
print(f"[Seedance Video] Starting video generation process...")
@@ -169,7 +173,7 @@ class FalAPI_seedance_video:
print(f"[Seedance Video] Input image shape: {image.shape}")
print(f"[Seedance Video] Prompt: {prompt}")
print(f"[Seedance Video] Settings: aspect_ratio={aspect_ratio}, resolution={resolution}, duration={duration}s")
print(f"[Seedance Video] Camera fixed: {camera_fixed}, Safety checker: {enable_safety_checker}")
print(f"[Seedance Video] Camera fixed: {camera_fixed}, Generate audio: {generate_audio}, Safety checker: {enable_safety_checker}")
try:
# 1) Set up FAL client with API token
@@ -228,6 +232,7 @@ class FalAPI_seedance_video:
"resolution": resolution,
"duration": duration,
"camera_fixed": camera_fixed,
"generate_audio": generate_audio,
"enable_safety_checker": enable_safety_checker
}
@@ -244,7 +249,7 @@ class FalAPI_seedance_video:
try:
result = await asyncio.to_thread(
fal_client.subscribe,
"fal-ai/bytedance/seedance/v1/pro/image-to-video",
"fal-ai/bytedance/seedance/v1.5/pro/image-to-video",
arguments=arguments,
with_logs=True,
on_queue_update=self.on_queue_update,
@@ -286,13 +291,14 @@ class FalAPI_seedance_video:
generation_info = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"model": "fal-ai/bytedance/seedance/v1/pro/image-to-video",
"model": "fal-ai/bytedance/seedance/v1.5/pro/image-to-video",
"parameters": {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"duration": duration,
"camera_fixed": camera_fixed,
"generate_audio": generate_audio,
"enable_safety_checker": enable_safety_checker,
"seed": seed if seed != -1 else seed_used
},
@@ -319,7 +325,7 @@ class FalAPI_seedance_video:
error_info = {
"error": f"Seedance video generation failed: {str(e)}",
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"model": "fal-ai/bytedance/seedance/v1/pro/image-to-video"
"model": "fal-ai/bytedance/seedance/v1.5/pro/image-to-video"
}
print(f"[Seedance Video] Error: {str(e)}")
return (None, empty_path, json.dumps(error_info, indent=2))