Files
gokayfem-ComfyUI-fal-API/nodes/video_node.py
T

3807 lines
170 KiB
Python

import asyncio
import os
import tempfile
import cv2
import requests
import torch
from fal_client import AsyncClient
from .fal_utils import (
ApiHandler,
FalApiError,
FalConfig,
ImageUtils,
MediaUtils,
logger,
)
# Initialize FalConfig
fal_config = FalConfig()
class MiniMaxNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"minimax/video-01-live", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
}
result = ApiHandler.submit_and_get_result(
"fal-ai/minimax/video-01-live/image-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"minimax/video-01-live", e
)
class MiniMaxTextToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt):
try:
arguments = {
"prompt": prompt,
}
result = ApiHandler.submit_and_get_result("fal-ai/minimax-video", arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error("minimax-video", e)
class MiniMaxSubjectReferenceNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"subject_reference_image": ("IMAGE",{"tooltip": "Image of the subject to keep consistent in the generated video."}),
"prompt_optimizer": ("BOOLEAN", {"default": True, "tooltip": "Let MiniMax automatically optimize the prompt before generation."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, subject_reference_image, prompt_optimizer):
try:
image_url = ImageUtils.upload_image(subject_reference_image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"minimax/video-01-subject-reference",
"Failed to upload subject reference image",
)
arguments = {
"prompt": prompt,
"subject_reference_image_url": image_url,
"prompt_optimizer": prompt_optimizer,
}
result = ApiHandler.submit_and_get_result(
"fal-ai/minimax/video-01-subject-reference", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"minimax/video-01-subject-reference", e
)
class KlingNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, aspect_ratio, image=None):
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
}
try:
if image is not None:
image_url = ImageUtils.upload_image(image)
if image_url:
arguments["image_url"] = image_url
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v1/standard/image-to-video", arguments
)
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v1/standard", "Failed to upload image"
)
else:
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v1/standard/text-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v1/standard", e
)
class KlingPro10Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"tail_image": ("IMAGE",{"tooltip": "Optional image the video should end on (final frame)."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self, prompt, duration, aspect_ratio, image=None, tail_image=None
):
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
}
try:
if image is not None:
image_url = ImageUtils.upload_image(image)
if image_url:
arguments["image_url"] = image_url
# Handle tail image if provided
if tail_image is not None:
tail_image_url = ImageUtils.upload_image(tail_image)
if tail_image_url:
arguments["tail_image_url"] = tail_image_url
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v1/pro", "Failed to upload tail image"
)
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v1/pro/image-to-video", arguments
)
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v1/pro", "Failed to upload image"
)
else:
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v1/pro/text-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v1/pro", e
)
class KlingPro16Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"tail_image": ("IMAGE",{"tooltip": "Optional image the video should end on (final frame)."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self, prompt, duration, aspect_ratio, image=None, tail_image=None
):
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
}
try:
if image is not None:
image_url = ImageUtils.upload_image(image)
if image_url:
arguments["image_url"] = image_url
# Handle tail image if provided
if tail_image is not None:
tail_image_url = ImageUtils.upload_image(tail_image)
if tail_image_url:
arguments["tail_image_url"] = tail_image_url
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v1.6/pro", "Failed to upload tail image"
)
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v1.6/pro/image-to-video", arguments
)
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v1.6/pro", "Failed to upload image"
)
else:
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v1.6/pro/text-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v1.6/pro", e
)
class KlingMasterNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, aspect_ratio, image=None):
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
}
try:
if image is not None:
image_url = ImageUtils.upload_image(image)
if image_url:
arguments["image_url"] = image_url
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v2/master/image-to-video", arguments
)
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v2/master", "Failed to upload image"
)
else:
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v2/master/text-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v2/master", e
)
class KlingOmniImageToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"start_image": ("IMAGE",{"tooltip": "Image used as the first frame of the video."}),
},
"optional": {
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, start_image, end_image=None, duration="5", variations=1):
try:
start_image_url = ImageUtils.upload_image(start_image)
if not start_image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/image-to-video", "Failed to upload start image"
)
arguments = {
"prompt": prompt,
"start_image_url": start_image_url,
"duration": duration,
}
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
else:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/image-to-video", "Failed to upload end image"
)
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/kling-video/o1/image-to-video", arguments, variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/image-to-video", e
)
class KlingOmniReferenceToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
},
"optional": {
"reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Reference image(s) uploaded to guide generation."}),
"element_1_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 1 (character/object to include in the video)."}),
"element_1_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 1."}),
"element_2_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 2 (character/object to include in the video)."}),
"element_2_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 2."}),
"element_3_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 3 (character/object to include in the video)."}),
"element_3_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 3."}),
"element_4_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 4 (character/object to include in the video)."}),
"element_4_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 4."}),
"element_5_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 5 (character/object to include in the video)."}),
"element_5_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 5."}),
"element_6_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 6 (character/object to include in the video)."}),
"element_6_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 6."}),
"element_7_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 7 (character/object to include in the video)."}),
"element_7_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 7."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self,
prompt,
reference_images=None,
element_1_frontal_image=None,
element_1_reference_images=None,
element_2_frontal_image=None,
element_2_reference_images=None,
element_3_frontal_image=None,
element_3_reference_images=None,
element_4_frontal_image=None,
element_4_reference_images=None,
element_5_frontal_image=None,
element_5_reference_images=None,
element_6_frontal_image=None,
element_6_reference_images=None,
element_7_frontal_image=None,
element_7_reference_images=None,
duration="5",
aspect_ratio="16:9",
variations=1
):
try:
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
}
# Handle reference images
if reference_images is not None:
ref_image_urls = ImageUtils.prepare_images(reference_images)
if ref_image_urls:
arguments["image_urls"] = ref_image_urls
# Build elements array
elements = []
# Process each element (up to 7)
for i in range(1, 8):
frontal_img = locals().get(f"element_{i}_frontal_image")
ref_imgs = locals().get(f"element_{i}_reference_images")
if frontal_img is not None:
element = {}
# Upload frontal image
frontal_url = ImageUtils.upload_image(frontal_img)
if frontal_url:
element["frontal_image_url"] = frontal_url
# Upload reference images if provided
if ref_imgs is not None:
ref_urls = ImageUtils.prepare_images(ref_imgs)
if ref_urls:
element["reference_image_urls"] = ref_urls
elements.append(element)
if elements:
arguments["elements"] = elements
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/kling-video/o1/reference-to-video", arguments, variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/reference-to-video", e
)
class KlingOmniVideoToVideoEditNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"video": ("VIDEO",{"tooltip": "Input video, uploaded to fal for processing."}),
},
"optional": {
"keep_audio": ("BOOLEAN", {"default": False, "tooltip": "Keep the audio track of the input video in the output."}),
"reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Reference image(s) uploaded to guide generation."}),
"element_1_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 1 (character/object to include in the video)."}),
"element_1_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 1."}),
"element_2_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 2 (character/object to include in the video)."}),
"element_2_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 2."}),
"element_3_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 3 (character/object to include in the video)."}),
"element_3_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 3."}),
"element_4_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 4 (character/object to include in the video)."}),
"element_4_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 4."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
prompt,
video,
keep_audio=False,
reference_images=None,
element_1_frontal_image=None,
element_1_reference_images=None,
element_2_frontal_image=None,
element_2_reference_images=None,
element_3_frontal_image=None,
element_3_reference_images=None,
element_4_frontal_image=None,
element_4_reference_images=None,
variations=1
):
try:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/video-to-video/edit", "Failed to upload video"
)
arguments = {
"prompt": prompt,
"video_url": video_url,
"keep_audio": keep_audio,
}
# Handle reference images
if reference_images is not None:
ref_image_urls = ImageUtils.prepare_images(reference_images)
if ref_image_urls:
arguments["image_urls"] = ref_image_urls
# Build elements array
elements = []
# Process each element (up to 4 for video-to-video/edit)
for i in range(1, 5):
frontal_img = locals().get(f"element_{i}_frontal_image")
ref_imgs = locals().get(f"element_{i}_reference_images")
if frontal_img is not None:
element = {}
# Upload frontal image
frontal_url = ImageUtils.upload_image(frontal_img)
if frontal_url:
element["frontal_image_url"] = frontal_url
# Upload reference images if provided
if ref_imgs is not None:
ref_urls = ImageUtils.prepare_images(ref_imgs)
if ref_urls:
element["reference_image_urls"] = ref_urls
elements.append(element)
if elements:
arguments["elements"] = elements
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/kling-video/o1/video-to-video/edit", arguments, variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/video-to-video/edit", e
)
class KlingOmniVideoToVideoReferenceNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"video": ("VIDEO",{"tooltip": "Input video, uploaded to fal for processing."}),
},
"optional": {
"keep_audio": ("BOOLEAN", {"default": False, "tooltip": "Keep the audio track of the input video in the output."}),
"reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Reference image(s) uploaded to guide generation."}),
"element_1_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 1 (character/object to include in the video)."}),
"element_1_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 1."}),
"element_2_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 2 (character/object to include in the video)."}),
"element_2_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 2."}),
"element_3_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 3 (character/object to include in the video)."}),
"element_3_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 3."}),
"element_4_frontal_image": ("IMAGE",{"tooltip": "Frontal image of element 4 (character/object to include in the video)."}),
"element_4_reference_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Additional reference images for element 4."}),
"aspect_ratio": (["auto", "16:9", "9:16", "1:1"], {"default": "auto", "tooltip": "Aspect ratio of the generated video."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self,
prompt,
video,
keep_audio=False,
reference_images=None,
element_1_frontal_image=None,
element_1_reference_images=None,
element_2_frontal_image=None,
element_2_reference_images=None,
element_3_frontal_image=None,
element_3_reference_images=None,
element_4_frontal_image=None,
element_4_reference_images=None,
aspect_ratio="auto",
duration="5",
variations=1
):
try:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/video-to-video/reference", "Failed to upload video"
)
arguments = {
"prompt": prompt,
"video_url": video_url,
"keep_audio": keep_audio,
"aspect_ratio": aspect_ratio,
"duration": duration,
}
# Handle reference images
if reference_images is not None:
ref_image_urls = ImageUtils.prepare_images(reference_images)
if ref_image_urls:
arguments["image_urls"] = ref_image_urls
# Build elements array
elements = []
# Process each element (up to 4 for video-to-video/reference)
for i in range(1, 5):
frontal_img = locals().get(f"element_{i}_frontal_image")
ref_imgs = locals().get(f"element_{i}_reference_images")
if frontal_img is not None:
element = {}
# Upload frontal image
frontal_url = ImageUtils.upload_image(frontal_img)
if frontal_url:
element["frontal_image_url"] = frontal_url
# Upload reference images if provided
if ref_imgs is not None:
ref_urls = ImageUtils.prepare_images(ref_imgs)
if ref_urls:
element["reference_image_urls"] = ref_urls
elements.append(element)
if elements:
arguments["elements"] = elements
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/kling-video/o1/video-to-video/reference", arguments, variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/o1/video-to-video/reference", e
)
class RunwayGen3Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, duration):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"runway-gen3", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
}
result = ApiHandler.submit_and_get_result(
"fal-ai/runway-gen3/turbo/image-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error("runway-gen3", e)
class LumaDreamMachineNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"mode": (
["text-to-video", "image-to-video"],
{"default": "text-to-video", "tooltip": "Operation mode for this node."},
),
"aspect_ratio": (
["16:9", "9:16", "4:3", "3:4", "21:9", "9:21"],
{"default": "16:9", "tooltip": "Aspect ratio of the generated video."},
),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"loop": ("BOOLEAN", {"default": False, "tooltip": "Make the generated video loop seamlessly."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self, prompt, mode, aspect_ratio, image=None, end_image=None, loop=False
):
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"loop": loop,
}
try:
if mode == "image-to-video":
if image is None:
return ApiHandler.handle_video_generation_error(
"luma-dream-machine",
"Image is required for image-to-video mode",
)
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"luma-dream-machine", "Failed to upload image"
)
arguments["image_url"] = image_url
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
else:
return ApiHandler.handle_video_generation_error(
"luma-dream-machine", "Failed to upload end image"
)
endpoint = "fal-ai/luma-dream-machine/ray-2/image-to-video"
else:
endpoint = "fal-ai/luma-dream-machine/ray-2"
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"luma-dream-machine", e
)
class Veo2ImageToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"aspect_ratio": (
["auto", "auto_prefer_portrait", "16:9", "9:16"],
{"default": "auto", "tooltip": "Aspect ratio of the generated video."},
),
"duration": (["5s", "6s", "7s", "8s"], {"default": "5s", "tooltip": "Length of the generated video in seconds."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, aspect_ratio, duration):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"veo2", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"aspect_ratio": aspect_ratio,
"duration": duration,
}
result = ApiHandler.submit_and_get_result(
"fal-ai/veo2/image-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error("veo2", e)
class WanProNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the content safety checker on the input."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, seed=0, enable_safety_checker=True, variations=1):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"wan-pro", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"enable_safety_checker": enable_safety_checker,
}
# Only add seed if it's not 0 (default)
if seed != 0:
arguments["seed"] = seed
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/wan-pro/image-to-video", arguments, variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error("wan-pro", e)
class Wan25Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"resolution": (
["480p", "720p", "1080p"],
{"default": "1080p", "tooltip": "Output video resolution."}),
"duration": (
["5", "10"],
{"default": "5", "tooltip": "Length of the generated video in seconds."}),
"negative_prompt": ("STRING", {"default": "low resolution, error, worst quality, low quality, defects", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"enable_prompt_expansion": ("BOOLEAN", {"default": True, "tooltip": "Let the model expand/rewrite the prompt for better results."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self,
prompt,
image,
seed=0,
resolution="1080p",
duration="5",
negative_prompt="low resolution, error, worst quality, low quality, defects",
enable_prompt_expansion=True,
variations=1,
):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"wan-25", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"resolution": resolution,
"duration": duration,
"negative_prompt": negative_prompt,
"enable_prompt_expansion": enable_prompt_expansion,
}
# include seed if non-default
if seed != 0:
arguments["seed"] = seed
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/wan-25-preview/image-to-video", arguments, variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error("wan-25", e)
class WanVACEVideoEditNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
},
"optional": {
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
"input_video_url": ("STRING", {"default": "", "tooltip": "URL of an input video, used instead of the VIDEO input when set."}),
"images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Optional image(s) uploaded and passed to the endpoint as image_urls."}),
"video_type": (["auto", "general", "human"], {"default": "auto", "tooltip": "Type of input video content, affects preprocessing."}),
"resolution": (
["auto", "240p", "360p", "480p", "580p", "720p", "1080p"],
{"default": "auto", "tooltip": "Output video resolution."}
),
"acceleration": (["regular", "low", "none", "default"], {"default": "regular", "tooltip": "Inference acceleration level; higher is faster but may reduce quality."}),
"enable_auto_downsample": ("BOOLEAN", {"default": True, "tooltip": "Automatically downsample the input video FPS when needed."}),
"aspect_ratio": (["auto", "16:9", "9:16", "1:1"], {"default": "auto", "tooltip": "Aspect ratio of the generated video."}),
"auto_downsample_min_fps": ("INT", {"default": 15, "min": 1, "max": 60, "tooltip": "Minimum FPS allowed when auto-downsampling."}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the content safety checker on the input."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
prompt,
video=None,
input_video_url="",
images=None,
video_type="auto",
resolution="auto",
acceleration="regular",
enable_auto_downsample=True,
aspect_ratio="auto",
auto_downsample_min_fps=15,
enable_safety_checker=True,
variations=1,
):
try:
if video is None and input_video_url == "":
return ApiHandler.handle_video_generation_error(
"wan-vace", "Video or Video Frames input is required."
)
if video is None and input_video_url != "":
video_url = input_video_url
else:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"wan-vace", "Failed to upload video"
)
arguments = {
"prompt": prompt,
"video_url": video_url,
"video_type": video_type,
"resolution": resolution,
"acceleration": acceleration,
"enable_auto_downsample": enable_auto_downsample,
"aspect_ratio": aspect_ratio,
"auto_downsample_min_fps": auto_downsample_min_fps,
"enable_safety_checker": enable_safety_checker,
}
image_urls = []
if images is not None:
if isinstance(images, torch.Tensor):
if images.ndim == 4 and images.shape[0] > 1:
for i in range(images.shape[0]):
single_img = images[i:i+1]
img_url = ImageUtils.upload_image(single_img)
if img_url:
image_urls.append(img_url)
else:
img_url = ImageUtils.upload_image(images)
if img_url:
image_urls.append(img_url)
elif isinstance(images, (list, tuple)):
for img in images:
img_url = ImageUtils.upload_image(img)
if img_url:
image_urls.append(img_url)
if image_urls:
arguments["image_urls"] = image_urls
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/wan-vace-apps/video-edit",
arguments,
variations
)
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error("wan-vace", e)
class Wan2214bAnimateReplaceNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {"default": None, "tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
"optional": {
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
"input_video_url": ("STRING", {"default": "", "tooltip": "URL of an input video, used instead of the VIDEO input when set."}),
"turbo": ("BOOLEAN", {"default": True, "tooltip": "Use the faster turbo mode."}),
"resolution": (
["480p", "580p", "720p"],
{"default": "480p", "tooltip": "Output video resolution."}
),
"seed": ("INT", {"default": 24, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"num_inference_steps": ("INT", {"default": 20, "min": 1, "max": 40, "step": 1, "tooltip": "Number of diffusion sampling steps."}),
"guidance_scale": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 10.0, "step": 0.1, "tooltip": "Classifier-free guidance scale; how strictly to follow the prompt."}),
"shift": ("INT", {"default": 8, "min": 1, "max": 10, "step": 1, "tooltip": "Sampler shift parameter."}),
"video_quality": (["low", "medium", "high", "maximum"], {"default": "high", "tooltip": "Output video encoding quality."}),
"video_write_mode": (["balanced", "fast", "small"], {"default": "balanced", "tooltip": "Encoding trade-off between speed, file size and quality."}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the content safety checker on the input."}),
"enable_output_safety_checker": ("BOOLEAN", {"default": False, "tooltip": "Run the safety checker on the generated output."}),
"return_frames_zip": ("BOOLEAN", {"default": False, "tooltip": "Also return a ZIP archive of the raw output frames."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING","STRING")
RETURN_NAMES = ("video_url","frames_zip_url",)
OUTPUT_IS_LIST = (True,True)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
image=None,
video=None,
input_video_url="",
turbo=True,
resolution="480p",
seed=24,
num_inference_steps=20,
guidance_scale=1.0,
shift=8,
video_quality="high",
video_write_mode="balanced",
enable_safety_checker=True,
enable_output_safety_checker=False,
return_frames_zip=False,
variations=1,
):
try:
if video is None and input_video_url == "":
return ApiHandler.handle_video_generation_error(
"wan-22animatereplace", "Video or Video Frames input is required."
)
if video is None and input_video_url != "":
video_url = input_video_url
else:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"wan-22animatereplace", "Failed to upload video"
)
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"wan-22animatereplace", "Failed to upload image"
)
arguments={
"video_url": video_url,
"image_url": image_url,
"turbo": turbo,
"resolution": resolution,
"seed": seed,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"shift": shift,
"video_quality": video_quality,
"video_write_mode": video_write_mode,
"enable_safety_checker": enable_safety_checker,
"enable_output_safety_checker": enable_output_safety_checker,
"return_frames_zip": return_frames_zip,
}
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/wan/v2.2-14b/animate/replace",
arguments,
variations
)
video_url = [r["video"]["url"] for r in results]
frames_zip_url = [r.get("frames_zip", {}).get("url", "") for r in results] if return_frames_zip else [""] * len(results)
return (video_url, frames_zip_url)
except Exception as e:
return ApiHandler.handle_video_generation_error("wan-22animatereplace", e)
class Wan2214bAnimateMoveNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {"default": None, "tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
"optional": {
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
"input_video_url": ("STRING", {"default": "", "tooltip": "URL of an input video, used instead of the VIDEO input when set."}),
"turbo": ("BOOLEAN", {"default": True, "tooltip": "Use the faster turbo mode."}),
"resolution": (
["480p", "580p", "720p"],
{"default": "480p", "tooltip": "Output video resolution."}
),
"seed": ("INT", {"default": 24, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"num_inference_steps": ("INT", {"default": 20, "min": 1, "max": 40, "step": 1, "tooltip": "Number of diffusion sampling steps."}),
"guidance_scale": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 10.0, "step": 0.1, "tooltip": "Classifier-free guidance scale; how strictly to follow the prompt."}),
"shift": ("INT", {"default": 8, "min": 1, "max": 10, "step": 1, "tooltip": "Sampler shift parameter."}),
"video_quality": (["low", "medium", "high", "maximum"], {"default": "high", "tooltip": "Output video encoding quality."}),
"video_write_mode": (["balanced", "fast", "small"], {"default": "balanced", "tooltip": "Encoding trade-off between speed, file size and quality."}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the content safety checker on the input."}),
"enable_output_safety_checker": ("BOOLEAN", {"default": False, "tooltip": "Run the safety checker on the generated output."}),
"return_frames_zip": ("BOOLEAN", {"default": False, "tooltip": "Also return a ZIP archive of the raw output frames."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("video_url","frames_zip_url",)
OUTPUT_IS_LIST = (True,True)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
image=None,
video=None,
input_video_url="",
turbo=True,
resolution="480p",
seed=24,
num_inference_steps=20,
guidance_scale=1.0,
shift=8,
video_quality="high",
video_write_mode="balanced",
enable_safety_checker=True,
enable_output_safety_checker=False,
return_frames_zip=False,
variations=1,
):
try:
if video is None and input_video_url == "":
return ApiHandler.handle_video_generation_error(
"wan-22animatereplace", "Video or Video Frames input is required."
)
if video is None and input_video_url != "":
video_url = input_video_url
else:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"wan-22animatereplace", "Failed to upload video"
)
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"wan-22animatemove", "Failed to upload image"
)
arguments={
"video_url": video_url,
"image_url": image_url,
"turbo": turbo,
"resolution": resolution,
"seed": seed,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"shift": shift,
"video_quality": video_quality,
"video_write_mode": video_write_mode,
"enable_safety_checker": enable_safety_checker,
"enable_output_safety_checker": enable_output_safety_checker,
"return_frames_zip": return_frames_zip,
}
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/wan/v2.2-14b/animate/move",
arguments,
variations
)
video_url = [r["video"]["url"] for r in results]
frames_zip_url = [r.get("frames_zip", {}).get("url", "") for r in results] if return_frames_zip else [""] * len(results)
return (video_url, frames_zip_url)
except Exception as e:
return ApiHandler.handle_video_generation_error("wan-22animatemove", e)
class Wan22VACEFun14bNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "tooltip": "Text prompt describing the desired video content and motion."}),
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
},
"optional": {
"task": (["depth", "pose"], {"default": "depth", "tooltip": "Control task used for preprocessing (depth or pose)."}),
"preprocess": ("BOOLEAN", {"default": True, "tooltip": "Preprocess the input video into control maps."}),
"ref_images": ("IMAGE", {"default": None, "multiple": True, "tooltip": "Reference images uploaded to guide generation."}),
"first_frame": ("IMAGE", {"default": None, "tooltip": "Image used as the first frame of the video."}),
"last_frame": ("IMAGE", {"default": None, "tooltip": "Optional image used as the last frame of the video."}),
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"seed": ("INT", {"default": 24, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"resolution": (
["480p", "580p", "720p"],
{"default": "480p", "tooltip": "Output video resolution."}
),
"aspect_ratio": (["auto", "16:9", "9:16", "1:1"], {"default": "auto", "tooltip": "Aspect ratio of the generated video."}),
"num_inference_steps": ("INT", {"default": 30, "min": 1, "tooltip": "Number of diffusion sampling steps."}),
"guidance_scale": ("FLOAT", {"default": 5, "min": 0.0, "max": 10, "tooltip": "Classifier-free guidance scale; how strictly to follow the prompt."}),
"sampler": (["unipc", "dpm++", "euler"], {"default": "unipc", "tooltip": "Diffusion sampler to use."}),
"match_input_num_frames": ("BOOLEAN", {"default": False, "tooltip": "Match the output frame count to the input video."}),
"num_frames": ("INT", {"default": 81, "min": 17, "max": 241, "tooltip": "Number of frames to generate."}),
"match_input_frames_per_second": ("BOOLEAN", {"default": False, "tooltip": "Match the output FPS to the input video."}),
"frames_per_second": ("INT", {"default": 16, "min": 5, "max": 30, "tooltip": "Frames per second of the generated video."}),
"shift": ("INT", {"default": 5, "tooltip": "Sampler shift parameter."}),
"acceleration": (["none", "low", "regular"], {"default": "regular", "tooltip": "Inference acceleration level; higher is faster but may reduce quality."}),
"video_quality": (["low", "medium", "high"], {"default": "high", "tooltip": "Output video encoding quality."}),
"video_write_mode": (["balanced", "fast", "small"], {"default": "balanced", "tooltip": "Encoding trade-off between speed, file size and quality."}),
"return_frames_zip": ("BOOLEAN", {"default": False, "tooltip": "Also return a ZIP archive of the raw output frames."}),
"num_interpolated_frames": ("INT", {"default": 0, "min": 0, "max": 5, "tooltip": "Number of frames to interpolate between generated frames."}),
"temporal_downsample_factor": ("INT", {"default": 0, "min": 0, "max": 5, "tooltip": "Factor for temporally downsampling the input video."}),
"enable_auto_downsample": ("BOOLEAN", {"default": False, "tooltip": "Automatically downsample the input video FPS when needed."}),
"auto_downsample_min_fps": ("INT", {"default": 15, "min": 1, "max": 60, "tooltip": "Minimum FPS allowed when auto-downsampling."}),
"interpolator_model": (["rife", "film"], {"default": "film", "tooltip": "Model used for frame interpolation."}),
"enable_safety_checker": ("BOOLEAN", {"default": False, "tooltip": "Run the content safety checker on the input."}),
"enable_output_safety_checker": ("BOOLEAN", {"default": False, "tooltip": "Run the safety checker on the generated output."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
},
}
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("video_url", "frames_zip_url",)
OUTPUT_IS_LIST = (True, True,)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
prompt="",
video=None,
task="depth",
preprocess=True,
ref_images=None,
first_frame=None,
last_frame=None,
negative_prompt="",
seed=-1,
resolution="480p",
aspect_ratio="auto",
num_inference_steps=30,
guidance_scale=5,
sampler="unipc",
match_input_num_frames=False,
num_frames=81,
match_input_frames_per_second=False,
frames_per_second=16,
shift=5,
acceleration="regular",
video_quality="high",
video_write_mode="balanced",
return_frames_zip=False,
num_interpolated_frames=0,
temporal_downsample_factor=0,
enable_auto_downsample=False,
auto_downsample_min_fps=15,
interpolator_model="film",
enable_safety_checker=False,
enable_output_safety_checker=False,
variations=1,
):
try:
if video is None:
return ApiHandler.handle_video_generation_error(
"wan-22-vace-fun-a14b", "Video input is required."
)
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"wan-22-vace-fun-a14b", "Failed to upload video"
)
# Build arguments
arguments = {
"prompt": prompt,
"video_url": video_url,
"preprocess": preprocess,
"negative_prompt": negative_prompt,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"sampler": sampler,
"num_frames": num_frames,
"frames_per_second": frames_per_second,
"shift": shift,
"acceleration": acceleration,
"video_quality": video_quality,
"video_write_mode": video_write_mode,
"return_frames_zip": return_frames_zip,
"num_interpolated_frames": num_interpolated_frames,
"temporal_downsample_factor": temporal_downsample_factor,
"enable_auto_downsample": enable_auto_downsample,
"auto_downsample_min_fps": auto_downsample_min_fps,
"interpolator_model": interpolator_model,
"enable_safety_checker": enable_safety_checker,
"enable_output_safety_checker": enable_output_safety_checker,
}
# Set seed
if seed != -1:
arguments["seed"] = seed
# Handle optional match_input settings
if match_input_num_frames and video is not None:
try:
arguments["num_frames"] = len(list(video.get_stream()))
except Exception as e:
logger.warning(
f"wan-22-vace-fun-a14b: could not read input frame count, "
f"falling back to num_frames={num_frames}: {e}"
)
if match_input_frames_per_second and video is not None:
try:
arguments["frames_per_second"] = video.get_fps()
except Exception as e:
logger.warning(
f"wan-22-vace-fun-a14b: could not read input FPS, "
f"falling back to frames_per_second={frames_per_second}: {e}"
)
# Upload reference images if provided
if ref_images is not None:
ref_image_urls = ImageUtils.prepare_images(ref_images)
if ref_image_urls:
arguments["ref_image_urls"] = ref_image_urls
# Upload first frame if provided
if first_frame is not None:
first_frame_url = ImageUtils.upload_image(first_frame)
if first_frame_url:
arguments["first_frame_image_url"] = first_frame_url
# Upload last frame if provided
if last_frame is not None:
last_frame_url = ImageUtils.upload_image(last_frame)
if last_frame_url:
arguments["last_frame_image_url"] = last_frame_url
# Submit to API with task-specific endpoint
results = ApiHandler.submit_multiple_and_get_results(
f"fal-ai/wan-22-vace-fun-a14b/{task}",
arguments,
variations
)
# Return list of outputs
video_url = [r["video"]["url"] for r in results]
frames_zip_url = [r.get("frames_zip", {}).get("url", "") for r in results] if return_frames_zip else [""] * len(results)
return (video_url, frames_zip_url)
except Exception as e:
return ApiHandler.handle_video_generation_error("wan-22-vace-fun-a14b", e)
# =============================================================================
# HYPER CUSTOM DY ENDPOINTS
# =============================================================================
# These are specialized, highly customizable DY endpoints with extensive
# parameter sets for advanced video generation and manipulation tasks.
# =============================================================================
class DYWanFun22Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"architecture": (["vace", "control"], {"default": "vace", "tooltip": "Model architecture to use (vace or control)."}),
"control_video": ("VIDEO", {"default": None, "tooltip": "Control video guiding motion and structure."}),
"ref_image": ("IMAGE", {"default": None, "tooltip": "Reference image for the generated video."}),
},
"optional": {
"turbo_mode": ("BOOLEAN", {"default": True, "tooltip": "Use the faster turbo mode."}),
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"image_size": (["custom", "square_hd", "square", "portrait_4_3", "portrait_16_9", "landscape_4_3", "landscape_16_9"], {"default": "custom", "tooltip": "Output size preset, or 'custom' to use custom_width/custom_height."}),
"custom_width": ("INT", {"default": 1280, "min": 0, "max": 8192, "step": 8, "tooltip": "Width in pixels used when a custom size is selected."}),
"custom_height": ("INT", {"default": 720, "min": 0, "max": 8192, "step": 8, "tooltip": "Height in pixels used when a custom size is selected."}),
"num_frames": ("INT", {"default": 81, "min": 1, "max": 1000, "tooltip": "Number of frames to generate."}),
"frames_per_second": ("INT", {"default": 16, "min": 5, "max": 30, "tooltip": "Frames per second of the generated video."}),
"num_inference_steps": ("INT", {"default": 4, "min": 1, "max": 100, "tooltip": "Number of diffusion sampling steps."}),
"guidance_scale": ("FLOAT", {"default": 1, "min": 0.0, "max": 10.0, "tooltip": "Classifier-free guidance scale; how strictly to follow the prompt."}),
"seed": ("INT", {"default": -1, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"sampler": (["uni_pc", "dpmpp_2m", "dpmpp_2m_sde", "euler", "euler_ancestral"], {"default": "uni_pc", "tooltip": "Diffusion sampler to use."}),
"shift": ("INT", {"default": 5, "min": 0, "max": 10, "tooltip": "Sampler shift parameter."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."}),
# Control strengths
"vace_mask_video": ("VIDEO", {"default": None, "tooltip": "Mask video for VACE-guided editing."}),
"preprocess_all_maps": ("BOOLEAN", {"default": True, "tooltip": "Preprocess all provided control videos into control maps."}),
"strength_vace": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "tooltip": "Strength of VACE conditioning."}),
"pose_strength": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "tooltip": "Strength of pose control."}),
"pose_video": ("VIDEO", {"default": None, "multiple": True, "tooltip": "Pose control video."}),
"depth_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "tooltip": "Strength of depth control."}),
"depth_video": ("VIDEO", {"default": None, "multiple": True, "tooltip": "Depth control video."}),
"normal_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "tooltip": "Strength of normal-map control."}),
"normal_video": ("VIDEO", {"default": None, "multiple": True, "tooltip": "Normal-map control video."}),
"canny_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "tooltip": "Strength of canny-edge control."}),
"canny_video": ("VIDEO", {"default": None, "multiple": True, "tooltip": "Canny-edge control video."}),
# Advanced settings
"num_interpolated_frames": ("INT", {"default": 0, "min": 0, "max": 5, "tooltip": "Number of frames to interpolate between generated frames."}),
"temporal_downsample_factor": ("INT", {"default": 0, "min": 0, "max": 5, "tooltip": "Factor for temporally downsampling the input video."}),
"enable_auto_downsample": ("BOOLEAN", {"default": False, "tooltip": "Automatically downsample the input video FPS when needed."}),
"auto_downsample_min_fps": ("INT", {"default": 8, "min": 0, "max": 60, "tooltip": "Minimum FPS allowed when auto-downsampling."}),
"return_frames_zip": ("BOOLEAN", {"default": False, "tooltip": "Also return a ZIP archive of the raw output frames."}),
"lora_path_1": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights 1."}),
"lora_strength_1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1, "tooltip": "Strength of LoRA 1."}),
"lora_transformer_1": (["high", "low", "both"], {"default": "high", "tooltip": "Which transformer (high/low noise) LoRA 1 applies to."}),
"lora_path_2": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights 2."}),
"lora_strength_2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1, "tooltip": "Strength of LoRA 2."}),
"lora_transformer_2": (["high", "low", "both"], {"default": "high", "tooltip": "Which transformer (high/low noise) LoRA 2 applies to."}),
"lora_path_3": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights 3."}),
"lora_strength_3": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1, "tooltip": "Strength of LoRA 3."}),
"lora_transformer_3": (["high", "low", "both"], {"default": "high", "tooltip": "Which transformer (high/low noise) LoRA 3 applies to."}),
"lora_path_4": ("STRING", {"default": "", "tooltip": "URL or path of LoRA weights 4."}),
"lora_strength_4": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1, "tooltip": "Strength of LoRA 4."}),
"lora_transformer_4": (["high", "low", "both"], {"default": "high", "tooltip": "Which transformer (high/low noise) LoRA 4 applies to."}),
},
}
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("video_url", "frames_zip_url",)
OUTPUT_IS_LIST = (True, True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration/DY"
def generate_video(
self,
architecture="vace",
control_video=None,
ref_image=None,
turbo_mode=True,
prompt="",
negative_prompt="",
image_size="custom",
custom_width=1280,
custom_height=720,
num_frames=81,
frames_per_second=16,
num_inference_steps=4,
guidance_scale=1.0,
seed=-1,
sampler="uni_pc",
shift=5,
variations=1,
vace_mask_video=None,
preprocess_all_maps=True,
strength_vace=1.0,
pose_strength=0.6,
pose_video=None,
depth_strength=0.0,
depth_video=None,
normal_strength=0.0,
normal_video=None,
canny_strength=0.0,
canny_video=None,
num_interpolated_frames=0,
temporal_downsample_factor=0,
enable_auto_downsample=False,
auto_downsample_min_fps=8,
return_frames_zip=False,
lora_path_1="",
lora_strength_1=1.0,
lora_transformer_1="high",
lora_path_2="",
lora_strength_2=1.0,
lora_transformer_2="high",
lora_path_3="",
lora_strength_3=1.0,
lora_transformer_3="high",
lora_path_4="",
lora_strength_4=1.0,
lora_transformer_4="high",
):
try:
if ref_image is None:
return ApiHandler.handle_video_generation_error(
"dy-wan-fun-22", "Reference image is required."
)
# Upload reference image
ref_image_url = ImageUtils.upload_image(ref_image)
if not ref_image_url:
return ApiHandler.handle_video_generation_error(
"dy-wan-fun-22", "Failed to upload reference image"
)
# Build arguments
arguments = {
"ref_image_url": ref_image_url,
"architecture": architecture,
"turbo_mode": turbo_mode,
"prompt": prompt,
"negative_prompt": negative_prompt,
"num_frames": num_frames,
"frames_per_second": frames_per_second,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"sampler": sampler,
"shift": shift,
"preprocess_all_maps": preprocess_all_maps,
"return_frames_zip": return_frames_zip,
"pose_strength": pose_strength,
"depth_strength": depth_strength,
"normal_strength": normal_strength,
"canny_strength": canny_strength
}
# Set seed
if seed != -1:
arguments["seed"] = seed
# Handle image_size - use custom dimensions if provided, otherwise use aspect_ratio preset
if image_size == "custom":
arguments["image_size"] = {"width": custom_width, "height": custom_height}
else:
arguments["image_size"] = image_size
# Upload control video if provided
if control_video is not None:
control_video_url = ImageUtils.upload_file(control_video.get_stream_source())
if control_video_url:
arguments["control_video_url"] = control_video_url
# Handle VACE strength
if architecture == "vace":
arguments["strength_vace"] = strength_vace
# Handle VACE mask video
if vace_mask_video is not None:
vace_mask_video_url = ImageUtils.upload_file(vace_mask_video.get_stream_source())
if vace_mask_video_url:
arguments["vace_mask_video_url"] = vace_mask_video_url
# Upload and add pose video/strength if provided
if pose_video is not None:
pose_video_url = ImageUtils.upload_file(pose_video.get_stream_source())
if pose_video_url:
arguments["pose_video_url"] = pose_video_url
# Upload and add depth video/strength if provided
if depth_video is not None:
depth_video_url = ImageUtils.upload_file(depth_video.get_stream_source())
if depth_video_url:
arguments["depth_video_url"] = depth_video_url
# Upload and add normal video/strength if provided
if normal_video is not None:
normal_video_url = ImageUtils.upload_file(normal_video.get_stream_source())
if normal_video_url:
arguments["normal_video_url"] = normal_video_url
# Upload and add canny video/strength if provided
if canny_video is not None:
canny_video_url = ImageUtils.upload_file(canny_video.get_stream_source())
if canny_video_url:
arguments["canny_video_url"] = canny_video_url
# Add advanced interpolation settings if non-default
if num_interpolated_frames > 0:
arguments["num_interpolated_frames"] = num_interpolated_frames
if temporal_downsample_factor > 0:
arguments["temporal_downsample_factor"] = temporal_downsample_factor
if enable_auto_downsample:
arguments["enable_auto_downsample"] = enable_auto_downsample
arguments["auto_downsample_min_fps"] = auto_downsample_min_fps
# Add LoRAs if provided
loras = []
if lora_path_1:
loras.append({"url": lora_path_1, "strength": lora_strength_1, "transformer": lora_transformer_1})
if lora_path_2:
loras.append({"url": lora_path_2, "strength": lora_strength_2, "transformer": lora_transformer_2})
if lora_path_3:
loras.append({"url": lora_path_3, "strength": lora_strength_3, "transformer": lora_transformer_3})
if lora_path_4:
loras.append({"url": lora_path_4, "strength": lora_strength_4, "transformer": lora_transformer_4})
if loras:
arguments["loras"] = loras
# Submit to API
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/dy-wan-fun-22",
arguments,
variations
)
# Return list of outputs
video_url = [r["video"]["url"] for r in results]
frames_zip_url = [r.get("frames_zip", {}).get("url", "") for r in results] if return_frames_zip else [""] * len(results)
return (video_url, frames_zip_url)
except Exception as e:
return ApiHandler.handle_video_generation_error("dy-wan-fun-22", e)
class DYWanUpscalerNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
},
"optional": {
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
"video_url": ("STRING", {"default": "", "tooltip": "URL of the video to process."}),
"prompt": ("STRING", {"default": "cinematic composition, realistic high-quality photo, RAW photo, masterpiece, photorealistic, 8k", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"negative_prompt": ("STRING", {"default": "oversaturated, overexposed, static, blurry details", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"strength": ("FLOAT", {"default": 0.02, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How strongly to transform the input video; higher means more change."}),
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1, "tooltip": "Classifier-free guidance scale; how strictly to follow the prompt."}),
"num_inference_steps": ("INT", {"default": 10, "min": 1, "max": 50, "tooltip": "Number of diffusion sampling steps."}),
"seed": ("INT", {"default": -1, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"fps": ("INT", {"default": 24, "min": 1, "max": 120, "tooltip": "Output frames per second."}),
"image_size": (["custom", "landscape_16_9", "landscape_4_3", "portrait_16_9", "portrait_4_3", "square", "square_hd"], {"default": "custom", "tooltip": "Output size preset, or 'custom' to use custom_width/custom_height."}),
"custom_width": ("INT", {"default": 1920, "min": 0, "max": 8192, "step": 8, "tooltip": "Width in pixels used when a custom size is selected."}),
"custom_height": ("INT", {"default": 1080, "min": 0, "max": 8192, "step": 8, "tooltip": "Height in pixels used when a custom size is selected."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
FUNCTION = "upscale_video"
CATEGORY = "FAL/VideoGeneration/DY"
def upscale_video(
self,
video=None,
video_url="",
prompt="cinematic composition, realistic high-quality photo, RAW photo, masterpiece, photorealistic, 8k",
negative_prompt="oversaturated, overexposed, static, blurry details",
strength=0.02,
guidance_scale=3.5,
num_inference_steps=10,
seed=-1,
fps=24,
image_size="custom",
custom_width=1920,
custom_height=1080
):
try:
if video is None and video_url == "":
return ApiHandler.handle_video_generation_error(
"dy-wan-upscaler", "Video input is required."
)
# Upload video
if video:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"dy-wan-upscaler", "Failed to upload video"
)
# Build arguments
arguments = {
"video_url": video_url,
"prompt": prompt,
"negative_prompt": negative_prompt,
"strength": strength,
"guidance_scale": guidance_scale,
"num_inference_steps": num_inference_steps,
"fps": fps,
}
# Set seed
if seed != -1:
arguments["seed"] = seed
# Handle image_size - use custom dimensions if provided, otherwise use preset
if image_size == "custom":
arguments["image_size"] = {"width": custom_width, "height": custom_height}
else:
arguments["image_size"] = image_size
# Submit to API
result = ApiHandler.submit_and_get_result(
"fal-ai/dy-wan-upscaler",
arguments,
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error("dy-wan-upscaler", e)
# =============================================================================
# END HYPER CUSTOM DY ENDPOINTS
# =============================================================================
class PixverseSwapNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {"default": None, "tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"mode": (["person", "object", "background"], {"default": "person", "tooltip": "Operation mode for this node."}),
"keyframe_id": ("INT", {"default": 1, "tooltip": "Keyframe index used for the swap."}),
"quality": (
["360p", "540p", "720p"],
{"default": "720p", "tooltip": "Output video quality/resolution."}
),
"original_sound_switch": ("BOOLEAN", {"default": True, "tooltip": "Keep the original audio in the output video."}),
},
"optional": {
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
"input_video_url": ("STRING", {"default": "", "tooltip": "URL of an input video, used instead of the VIDEO input when set."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
video=None,
input_video_url="",
image=None,
keyframe_id=1,
quality="720p",
original_sound_switch=True,
mode="person"):
try:
if video is None and input_video_url == "":
return ApiHandler.handle_video_generation_error(
"pixverse-swap", "Video or Video Frames input is required."
)
if video is None and input_video_url != "":
video_url = input_video_url
else:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"pixverse-swap", "Failed to upload video"
)
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"pixverse-swap", "Failed to upload image"
)
arguments={
"video_url": video_url,
"image_url": image_url,
"keyframe_id": keyframe_id,
"quality": quality,
"original_sound_switch": original_sound_switch,
"mode": mode
}
result = ApiHandler.submit_and_get_result(
"fal-ai/pixverse/swap",
arguments,
)
return (result["video"]["url"],)
except Exception as e:
return ApiHandler.handle_video_generation_error("pixverse-swap", e)
class KreaWan14bVideoToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"enable_prompt_expansion": ("BOOLEAN", {"default": True, "tooltip": "Let the model expand/rewrite the prompt for better results."}),
"strength": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 3.0, "tooltip": "How strongly to transform the input video; higher means more change."}),
},
"optional": {
"video": ("VIDEO", {"default": None, "tooltip": "Input video, uploaded to fal for processing."}),
"input_video_url": ("STRING", {"default": "", "tooltip": "URL of an input video, used instead of the VIDEO input when set."}),
"seed": ("INT", {"default": 42, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
prompt="",
strength=0.85,
video=None,
input_video_url="",
seed=24,
enable_prompt_expansion=True,
):
try:
if video is None and input_video_url == "":
return ApiHandler.handle_video_generation_error(
"krea-wan-14b", "Video or Video URL input is required."
)
if video is None and input_video_url != "":
video_url = input_video_url
else:
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"krea-wan-14b", "Failed to upload video"
)
arguments={
"prompt": prompt,
"strength": strength,
"enable_prompt_expansion": enable_prompt_expansion,
"video_url": video_url,
"seed": seed
}
result = ApiHandler.submit_and_get_result(
"fal-ai/krea-wan-14b/video-to-video",
arguments,
)
return (result["video"]["url"],)
except Exception as e:
return ApiHandler.handle_video_generation_error("krea-wan-14b", e)
class InfinityStarTextToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"negative_prompt": ("STRING", {"default": "low quality.", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"aspect_ratio": (
["16:9", "9:6","1:1"],
{"default": "16:9", "tooltip": "Aspect ratio of the generated video."}
),
"guidance_scale": ("FLOAT", {"default": 7.5, "min": 0.0, "max": 10, "tooltip": "Classifier-free guidance scale; how strictly to follow the prompt."}),
},
"optional": {
"tau_video": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 2, "tooltip": "Tau parameter controlling video generation dynamics."}),
"enhance_prompt": ("BOOLEAN", {"default": True, "tooltip": "Let the model enhance the prompt automatically."}),
"seed": ("INT", {"default": 42, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
FUNCTION = "edit_video"
CATEGORY = "FAL/VideoGeneration"
def edit_video(
self,
prompt="",
negative_prompt="low quality.",
enhance_prompt=True,
seed=24,
aspect_ratio="16:9",
guidance_scale=7.5,
tau_video=0.4,
):
try:
arguments={
"prompt": prompt,
"negative_prompt": negative_prompt,
"aspect_ratio": aspect_ratio,
"guidance_scale": guidance_scale,
"enhance_prompt": enhance_prompt,
"tau_video": tau_video,
"seed": seed
}
result = ApiHandler.submit_and_get_result(
"fal-ai/infinity-star/text-to-video",
arguments,
)
return (result["video"]["url"],)
except Exception as e:
return ApiHandler.handle_video_generation_error("infinity-star-text-to-video", e)
class CombinedVideoGenerationNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"kling_duration": (["5", "10"], {"default": "5", "tooltip": "Duration in seconds for the Kling generations."}),
"kling_luma_aspect_ratio": (
["16:9", "9:16", "1:1"],
{"default": "16:9", "tooltip": "Aspect ratio used for the Kling and Luma generations."},
),
"luma_loop": ("BOOLEAN", {"default": False, "tooltip": "Make the Luma video loop seamlessly."}),
"veo2_aspect_ratio": (
["auto", "auto_prefer_portrait", "16:9", "9:16"],
{"default": "auto", "tooltip": "Aspect ratio used for the Veo2 generation."},
),
"veo2_duration": (["5s", "6s", "7s", "8s"], {"default": "5s", "tooltip": "Duration used for the Veo2 generation."}),
"enable_klingpro": ("BOOLEAN", {"default": True, "tooltip": "Also generate with Kling Pro v1.6."}),
"enable_klingmaster": ("BOOLEAN", {"default": True, "tooltip": "Also generate with Kling Master v2.0."}),
"enable_minimax": ("BOOLEAN", {"default": True, "tooltip": "Also generate with MiniMax."}),
"enable_luma": ("BOOLEAN", {"default": True, "tooltip": "Also generate with Luma Dream Machine."}),
"enable_veo2": ("BOOLEAN", {"default": True, "tooltip": "Also generate with Google Veo2."}),
"enable_wanpro": ("BOOLEAN", {"default": True, "tooltip": "Also generate with Wan Pro."}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = (
"klingpro_v1.6_video",
"klingmaster_v2.0_video",
"minimax_video",
"luma_video",
"veo2_video",
"wanpro_video",
)
FUNCTION = "generate_videos"
CATEGORY = "FAL/VideoGeneration"
async def generate_klingpro_video(
self, client, prompt, image_url, kling_duration, kling_luma_aspect_ratio
):
try:
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": kling_duration,
"aspect_ratio": kling_luma_aspect_ratio,
}
handler = await client.submit(
"fal-ai/kling-video/v1.6/pro/image-to-video", arguments=arguments
)
while True:
result = await handler.get()
if "video" in result and "url" in result["video"]:
return result["video"]["url"]
elif result.get("status") == "FAILED":
raise Exception("Video generation failed")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error generating KlingPro video: {str(e)}")
return "Error: Unable to generate KlingPro video."
async def generate_klingmaster_video(
self, client, prompt, image_url, kling_duration, kling_luma_aspect_ratio
):
try:
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": kling_duration,
"aspect_ratio": kling_luma_aspect_ratio,
}
handler = await client.submit(
"fal-ai/kling-video/v2/master/image-to-video", arguments=arguments
)
while True:
result = await handler.get()
if "video" in result and "url" in result["video"]:
return result["video"]["url"]
elif result.get("status") == "FAILED":
raise Exception("Video generation failed")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error generating KlingMaster video: {str(e)}")
return "Error: Unable to generate KlingMaster video."
async def generate_minimax_video(self, client, prompt, image_url):
try:
arguments = {
"prompt": prompt,
"image_url": image_url,
}
handler = await client.submit(
"fal-ai/minimax/video-01-live/image-to-video", arguments=arguments
)
while True:
result = await handler.get()
if "video" in result and "url" in result["video"]:
return result["video"]["url"]
elif result.get("status") == "FAILED":
raise Exception("Video generation failed")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error generating MiniMax video: {str(e)}")
return "Error: Unable to generate MiniMax video."
async def generate_luma_video(
self, client, prompt, image_url, kling_luma_aspect_ratio, luma_loop
):
try:
arguments = {
"prompt": prompt,
"image_url": image_url,
"aspect_ratio": kling_luma_aspect_ratio,
"loop": luma_loop,
}
handler = await client.submit(
"fal-ai/luma-dream-machine/ray-2/image-to-video", arguments=arguments
)
while True:
result = await handler.get()
if "video" in result and "url" in result["video"]:
return result["video"]["url"]
elif result.get("status") == "FAILED":
raise Exception("Video generation failed")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error generating Luma video: {str(e)}")
return "Error: Unable to generate Luma video."
async def generate_veo2_video(
self, client, prompt, image_url, aspect_ratio, duration
):
try:
arguments = {
"prompt": prompt,
"image_url": image_url,
"aspect_ratio": aspect_ratio,
"duration": duration,
}
handler = await client.submit(
"fal-ai/veo2/image-to-video", arguments=arguments
)
while True:
result = await handler.get()
if "video" in result and "url" in result["video"]:
return result["video"]["url"]
elif result.get("status") == "FAILED":
raise Exception("Video generation failed")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error generating Veo2 video: {str(e)}")
return "Error: Unable to generate Veo2 video."
async def generate_wanpro_video(self, client, prompt, image_url):
try:
arguments = {
"prompt": prompt,
"image_url": image_url,
"enable_safety_checker": True,
"seed": None, # Let the API choose a random seed
}
handler = await client.submit(
"fal-ai/wan-pro/image-to-video", arguments=arguments
)
while True:
result = await handler.get()
if "video" in result and "url" in result["video"]:
return result["video"]["url"]
elif result.get("status") == "FAILED":
raise Exception("Video generation failed")
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error generating Wan Pro video: {str(e)}")
return "Error: Unable to generate Wan Pro video."
async def generate_all_videos(
self,
prompt,
image_url,
kling_duration,
kling_luma_aspect_ratio,
luma_loop,
veo2_aspect_ratio,
veo2_duration,
enable_klingpro,
enable_klingmaster,
enable_minimax,
enable_luma,
enable_veo2,
enable_wanpro,
):
try:
tasks = []
results = [None] * 6 # Initialize results list with None values
# Create async client with the same key as the sync client
client = AsyncClient(key=fal_config.get_key())
# Add tasks based on enabled services
if enable_klingpro:
tasks.append(
self.generate_klingpro_video(
client,
prompt,
image_url,
kling_duration,
kling_luma_aspect_ratio,
)
)
else:
tasks.append(None)
if enable_klingmaster:
tasks.append(
self.generate_klingmaster_video(
client,
prompt,
image_url,
kling_duration,
kling_luma_aspect_ratio,
)
)
else:
tasks.append(None)
if enable_minimax:
tasks.append(self.generate_minimax_video(client, prompt, image_url))
else:
tasks.append(None)
if enable_luma:
tasks.append(
self.generate_luma_video(
client, prompt, image_url, kling_luma_aspect_ratio, luma_loop
)
)
else:
tasks.append(None)
if enable_veo2:
tasks.append(
self.generate_veo2_video(
client, prompt, image_url, veo2_aspect_ratio, veo2_duration
)
)
else:
tasks.append(None)
if enable_wanpro:
tasks.append(self.generate_wanpro_video(client, prompt, image_url))
else:
tasks.append(None)
# Filter out None tasks and execute them
valid_tasks = [task for task in tasks if task is not None]
if valid_tasks:
completed_results = await asyncio.gather(*valid_tasks)
# Place results in their correct positions
result_index = 0
for i, task in enumerate(tasks):
if task is not None:
results[i] = completed_results[result_index]
result_index += 1
else:
results[i] = "Service disabled"
return results
except Exception as e:
logger.error(f"Error in generate_all_videos: {str(e)}")
return ["Error: Unable to generate videos."] * 6
def generate_videos(
self,
prompt,
image,
kling_duration,
kling_luma_aspect_ratio,
luma_loop,
veo2_aspect_ratio,
veo2_duration,
enable_klingpro,
enable_klingmaster,
enable_minimax,
enable_luma,
enable_veo2,
enable_wanpro,
):
try:
# Upload image once to be used by all services
image_url = ImageUtils.upload_image(image)
if not image_url:
return ("Error: Unable to upload image.",) * 6
# Create event loop for async operations
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Run all video generations concurrently
results = loop.run_until_complete(
self.generate_all_videos(
prompt,
image_url,
kling_duration,
kling_luma_aspect_ratio,
luma_loop,
veo2_aspect_ratio,
veo2_duration,
enable_klingpro,
enable_klingmaster,
enable_minimax,
enable_luma,
enable_veo2,
enable_wanpro,
)
)
loop.close()
return tuple(results)
except Exception as e:
logger.error(f"Error in combined video generation: {str(e)}")
return ("Error: Unable to generate videos.",) * 6
class VideoUpscalerNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_url": ("STRING", {"default": "", "tooltip": "URL of the video to process."}),
"scale": (
"FLOAT",
{"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.5, "tooltip": "Upscale factor applied to the video."},
),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "upscale_video"
CATEGORY = "FAL/VideoGeneration"
def upscale_video(self, video_url, scale):
try:
arguments = {"video_url": video_url, "scale": scale}
result = ApiHandler.submit_and_get_result(
"fal-ai/video-upscaler", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error("video-upscaler", e)
class UploadVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video": ("VIDEO",{"tooltip": "Input video, uploaded to fal for processing."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
FUNCTION = "upload_video"
CATEGORY = "FAL/Video"
def upload_video(self, video):
video_url = ImageUtils.upload_file(video.get_stream_source())
return (video_url,)
class UploadFileNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"path": ("STRING",{"tooltip": "Local file path to upload to fal storage."}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("file_url",)
FUNCTION = "upload_file"
CATEGORY = "FAL/Video"
def upload_file(self, path):
file_url = ImageUtils.upload_file(path)
return (file_url,)
class LoadVideoURL:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"url": ("STRING", {"default": "", "tooltip": "HTTP(S) URL of the video to download and decode into frames."}),
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1, "tooltip": "Force a specific frame rate; 0 keeps the source FPS."}),
"force_size": (
[
"Disabled",
"Custom Height",
"Custom Width",
"Custom",
"256x?",
"?x256",
"256x256",
"512x?",
"?x512",
"512x512",
],
{"tooltip": "Resize mode applied to the loaded frames."}),
"custom_width": (
"INT",
{"default": 512, "min": 0, "max": 8192, "step": 8, "tooltip": "Width in pixels used when a custom size is selected."},
),
"custom_height": (
"INT",
{"default": 512, "min": 0, "max": 8192, "step": 8, "tooltip": "Height in pixels used when a custom size is selected."},
),
"frame_load_cap": (
"INT",
{"default": 0, "min": 0, "max": 1000000, "step": 1, "tooltip": "Maximum number of frames to load; 0 means no limit."},
),
"skip_first_frames": (
"INT",
{"default": 0, "min": 0, "max": 1000000, "step": 1, "tooltip": "Number of frames to skip at the start of the video."},
),
"select_every_nth": (
"INT",
{"default": 1, "min": 1, "max": 1000000, "step": 1, "tooltip": "Load only every Nth frame."},
),
},
}
RETURN_TYPES = ("IMAGE", "INT", "VHS_VIDEOINFO")
RETURN_NAMES = ("frames", "frame_count", "video_info")
FUNCTION = "load_video_from_url"
CATEGORY = "FAL/Video"
def load_video_from_url(
self,
url,
force_rate,
force_size,
custom_width,
custom_height,
frame_load_cap,
skip_first_frames,
select_every_nth,
):
url = (url or "").strip()
if not url:
raise ValueError(
"LoadVideoURL: 'url' is empty. Provide an http(s) URL to a video file."
)
if not url.startswith(("http://", "https://")):
raise ValueError(
f"LoadVideoURL: invalid URL '{url}'. Only http(s) URLs are supported."
)
temp_file_path = None
cap = None
try:
# Download the video to a temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_file:
temp_file_path = temp_file.name
response = requests.get(url, stream=True, timeout=(10, 600))
response.raise_for_status()
for chunk in response.iter_content(chunk_size=8192):
temp_file.write(chunk)
# Load the video using OpenCV
cap = cv2.VideoCapture(temp_file_path)
# Get video properties
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
if fps == 0:
logger.warning(
"LoadVideoURL: video reports 0 FPS; source duration defaults to 0.0"
)
duration = 0.0
else:
duration = total_frames / fps
# Calculate target size
if force_size != "Disabled":
if force_size == "Custom Width":
new_height = int(height * (custom_width / width))
new_width = custom_width
elif force_size == "Custom Height":
new_width = int(width * (custom_height / height))
new_height = custom_height
elif force_size == "Custom":
new_width, new_height = custom_width, custom_height
else:
target_width, target_height = map(
int, force_size.replace("?", "0").split("x")
)
if target_width == 0:
new_width = int(width * (target_height / height))
new_height = target_height
else:
new_height = int(height * (target_width / width))
new_width = target_width
else:
new_width, new_height = width, height
frames = []
frame_count = 0
for i in range(total_frames):
ret, frame = cap.read()
if not ret:
break
if i < skip_first_frames:
continue
if (i - skip_first_frames) % select_every_nth != 0:
continue
if force_size != "Disabled":
frame = cv2.resize(frame, (new_width, new_height))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = torch.from_numpy(frame).float() / 255.0
frames.append(frame)
frame_count += 1
if frame_load_cap > 0 and frame_count >= frame_load_cap:
break
finally:
if cap is not None:
cap.release()
if temp_file_path is not None and os.path.exists(temp_file_path):
os.unlink(temp_file_path)
if not frames:
raise ValueError(
f"LoadVideoURL: no frames could be decoded from '{url}'. "
"Check that the URL points to a valid video file."
)
frames = torch.stack(frames)
loaded_fps = fps if force_rate == 0 else force_rate
loaded_duration = frame_count / loaded_fps if loaded_fps else 0.0
if not loaded_fps:
logger.warning(
"LoadVideoURL: loaded FPS is 0; loaded duration defaults to 0.0"
)
video_info = {
"source_fps": fps,
"source_frame_count": total_frames,
"source_duration": duration,
"source_width": width,
"source_height": height,
"loaded_fps": loaded_fps,
"loaded_frame_count": frame_count,
"loaded_duration": loaded_duration,
"loaded_width": new_width,
"loaded_height": new_height,
}
return (frames, frame_count, video_info)
class VideoFromURL:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_url": (
"STRING",
{
"default": "",
"forceInput": True,
"tooltip": "URL of a video file (e.g. the video_url output of a fal generator node) to load as a native ComfyUI VIDEO object.",
},
),
},
}
RETURN_TYPES = ("VIDEO",)
RETURN_NAMES = ("video",)
FUNCTION = "load"
CATEGORY = "FAL/Video"
def load(self, video_url):
if not video_url or not str(video_url).strip():
raise ValueError(
"VideoFromURL: 'video_url' is empty. Connect a video URL string."
)
video = MediaUtils.video_from_url(str(video_url).strip())
if video is None:
raise FalApiError(
"VideoFromURL",
"This ComfyUI version does not support the VIDEO type — update ComfyUI",
)
return (video,)
class SeedanceImageToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"resolution": (["480p", "720p"], {"default": "720p", "tooltip": "Output video resolution."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"camera_fixed": ("BOOLEAN", {"default": False, "tooltip": "Keep the camera static during generation."}),
},
"optional": {
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, resolution, duration, camera_fixed, seed=-1):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"fal-ai/bytedance/seedance/v1/lite/image-to-video",
"Failed to upload image",
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"resolution": resolution,
"duration": duration,
"camera_fixed": camera_fixed,
}
# Only add seed if it's not -1 (random)
if seed != -1:
arguments["seed"] = seed
result = ApiHandler.submit_and_get_result(
"fal-ai/bytedance/seedance/v1/lite/image-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"fal-ai/bytedance/seedance/v1/lite/image-to-video", e
)
class SeedanceTextToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"aspect_ratio": (["16:9", "4:3", "1:1", "9:21"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"resolution": (["480p", "720p"], {"default": "720p", "tooltip": "Output video resolution."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"camera_fixed": ("BOOLEAN", {"default": False, "tooltip": "Keep the camera static during generation."}),
},
"optional": {
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, aspect_ratio, resolution, duration, camera_fixed, seed=-1):
try:
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"duration": duration,
"camera_fixed": camera_fixed,
}
# Only add seed if it's not -1 (random)
if seed != -1:
arguments["seed"] = seed
result = ApiHandler.submit_and_get_result(
"fal-ai/bytedance/seedance/v1/lite/text-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"fal-ai/bytedance/seedance/v1/lite/text-to-video", e
)
class SeedanceProImageToVideoNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"duration": (["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12",], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."})
},
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, duration, end_image=None, negative_prompt="", cfg_scale=0.5, variations=1):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"fal-ai/bytedance/seedance/v1/pro/image-to-video",
"Failed to upload image",
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale
}
# Handle optional End image
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
else:
return ApiHandler.handle_video_generation_error(
"seedance/v1/pro/image-to-video", "Failed to upload end image"
)
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/bytedance/seedance/v1/pro/image-to-video", arguments, variations
)
# Validate before exposing URLs to downstream loaders. Older error
# paths could leak an error string through this list output; a
# downstream loader would then receive its first character ("E")
# and raise requests.exceptions.MissingSchema.
endpoint = "fal-ai/bytedance/seedance/v1/pro/image-to-video"
video_urls = [
MediaUtils.require_http_url(r["video"]["url"], endpoint)
for r in results
]
return (video_urls,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"fal-ai/bytedance/seedance/v1/pro/image-to-video", e
)
class Veo3Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"duration": (["8s"], {"default": "8s", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"enhance_prompt": ("BOOLEAN", {"default": True, "tooltip": "Let the model enhance the prompt automatically."}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(
self,
prompt,
aspect_ratio,
duration,
negative_prompt="",
enhance_prompt=True,
seed=-1,
generate_audio=True,
):
arguments = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"duration": duration,
"negative_prompt": negative_prompt,
"enhance_prompt": enhance_prompt,
"generate_audio": generate_audio,
}
if seed != -1:
arguments["seed"] = seed
try:
result = ApiHandler.submit_and_get_result("fal-ai/veo3", arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error("veo3", e)
class FalKling21ProImageToVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"negative_prompt": ("STRING", {"default": "blur, distort, and low quality", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"tail_image": ("IMAGE",{"tooltip": "Optional image the video should end on (final frame)."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, duration, negative_prompt="blur, distort, and low quality", cfg_scale=0.5, tail_image=None):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.1/pro", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
}
# Handle optional tail image
if tail_image is not None:
tail_image_url = ImageUtils.upload_image(tail_image)
if tail_image_url:
arguments["tail_image_url"] = tail_image_url
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.1/pro", "Failed to upload tail image"
)
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v2.1/pro/image-to-video", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.1/pro", e
)
class FalKling25TurboProImageToVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"negative_prompt": ("STRING", {"default": "blur, distort, and low quality", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"tail_image": ("IMAGE",{"tooltip": "Optional image the video should end on (final frame)."}),
"variations": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1, "tooltip": "Number of videos to generate in parallel with the same settings."})
},
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, duration, negative_prompt="blur, distort, and low quality", cfg_scale=0.5, tail_image=None, variations=1):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.5-turbo/pro", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
}
# Handle optional tail image
if tail_image is not None:
tail_image_url = ImageUtils.upload_image(tail_image)
if tail_image_url:
arguments["tail_image_url"] = tail_image_url
else:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.5-turbo/pro", "Failed to upload tail image"
)
results = ApiHandler.submit_multiple_and_get_results(
"fal-ai/kling-video/v2.5-turbo/pro/image-to-video", arguments, variations
)
# Return list of video URLs
return ([r["video"]["url"] for r in results],)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.5-turbo/pro", e
)
class FalKling26ProVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"negative_prompt": ("STRING", {"default": "blur, distort, and low quality", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, image=None, aspect_ratio="16:9", negative_prompt="blur, distort, and low quality", cfg_scale=0.5, generate_audio=True):
try:
# Conditional routing based on whether image is provided
if image is None:
# T2V mode: Use text-to-video endpoint
endpoint = "fal-ai/kling-video/v2.6/pro/text-to-video"
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
"generate_audio": generate_audio,
}
else:
# I2V mode: Use image-to-video endpoint
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.6/pro", "Failed to upload image"
)
endpoint = "fal-ai/kling-video/v2.6/pro/image-to-video"
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"negative_prompt": negative_prompt,
"generate_audio": generate_audio,
}
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v2.6/pro", e
)
class FalKlingV3StandardVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"negative_prompt": ("STRING", {"default": "blur, distort, and low quality", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, image=None, end_image=None, aspect_ratio="16:9", negative_prompt="blur, distort, and low quality", cfg_scale=0.5, generate_audio=True):
try:
if image is None:
endpoint = "fal-ai/kling-video/v3/standard/text-to-video"
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
"generate_audio": generate_audio,
}
else:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/standard", "Failed to upload image"
)
endpoint = "fal-ai/kling-video/v3/standard/image-to-video"
arguments = {
"prompt": prompt,
"start_image_url": image_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
"generate_audio": generate_audio,
}
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/standard", e
)
class FalKlingV3ProVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"negative_prompt": ("STRING", {"default": "blur, distort, and low quality", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, image=None, end_image=None, aspect_ratio="16:9", negative_prompt="blur, distort, and low quality", cfg_scale=0.5, generate_audio=True):
try:
if image is None:
endpoint = "fal-ai/kling-video/v3/pro/text-to-video"
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
"generate_audio": generate_audio,
}
else:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/pro", "Failed to upload image"
)
endpoint = "fal-ai/kling-video/v3/pro/image-to-video"
arguments = {
"prompt": prompt,
"start_image_url": image_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
"generate_audio": generate_audio,
}
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/pro", e
)
class FalKlingV3StandardMotionControl:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"video": ("VIDEO",{"tooltip": "Input video, uploaded to fal for processing."}),
"character_orientation": (["image", "video"], {"default": "image", "tooltip": "Whether character orientation follows the image or the motion video."}),
},
"optional": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"keep_original_sound": ("BOOLEAN", {"default": True, "tooltip": "Keep the original audio from the motion video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, image, video, character_orientation, prompt="", keep_original_sound=True):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/standard/motion-control", "Failed to upload image"
)
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/standard/motion-control", "Failed to upload video"
)
arguments = {
"image_url": image_url,
"video_url": video_url,
"character_orientation": character_orientation,
"keep_original_sound": keep_original_sound,
}
if prompt and prompt.strip():
arguments["prompt"] = prompt
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v3/standard/motion-control", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/standard/motion-control", e
)
class FalKlingV3ProMotionControl:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"video": ("VIDEO",{"tooltip": "Input video, uploaded to fal for processing."}),
"character_orientation": (["image", "video"], {"default": "image", "tooltip": "Whether character orientation follows the image or the motion video."}),
},
"optional": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"keep_original_sound": ("BOOLEAN", {"default": True, "tooltip": "Keep the original audio from the motion video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, image, video, character_orientation, prompt="", keep_original_sound=True):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/pro/motion-control", "Failed to upload image"
)
video_url = ImageUtils.upload_file(video.get_stream_source())
if not video_url:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/pro/motion-control", "Failed to upload video"
)
arguments = {
"image_url": image_url,
"video_url": video_url,
"character_orientation": character_orientation,
"keep_original_sound": keep_original_sound,
}
if prompt and prompt.strip():
arguments["prompt"] = prompt
result = ApiHandler.submit_and_get_result(
"fal-ai/kling-video/v3/pro/motion-control", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/v3/pro/motion-control", e
)
class FalKlingO3StandardVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"negative_prompt": ("STRING", {"default": "blur, distort, and low quality", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "Guidance scale; how strictly the video follows the prompt."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, image=None, end_image=None, aspect_ratio="16:9", negative_prompt="blur, distort, and low quality", cfg_scale=0.5, generate_audio=True):
try:
if image is None:
endpoint = "fal-ai/kling-video/o3/standard/text-to-video"
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
"generate_audio": generate_audio,
}
else:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/o3/standard", "Failed to upload image"
)
endpoint = "fal-ai/kling-video/o3/standard/image-to-video"
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"cfg_scale": cfg_scale,
"generate_audio": generate_audio,
}
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/o3/standard", e
)
class FalKlingO3ProVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"end_image": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, image=None, end_image=None, aspect_ratio="16:9", generate_audio=True):
try:
if image is None:
endpoint = "fal-ai/kling-video/o3/pro/text-to-video"
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
"generate_audio": generate_audio,
}
else:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"kling-video/o3/pro", "Failed to upload image"
)
endpoint = "fal-ai/kling-video/o3/pro/image-to-video"
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"generate_audio": generate_audio,
}
if end_image is not None:
end_image_url = ImageUtils.upload_image(end_image)
if end_image_url:
arguments["end_image_url"] = end_image_url
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"kling-video/o3/pro", e
)
class FalWan26Video:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"duration": (["5", "10", "15"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
},
"optional": {
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
"audio_url": ("STRING", {"default": "", "tooltip": "Optional URL of an audio track to accompany the video."}),
"aspect_ratio": (["16:9", "9:16", "1:1", "4:3", "3:4"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"resolution": (["720p", "1080p"], {"default": "1080p", "tooltip": "Output video resolution."}),
"negative_prompt": ("STRING", {"default": "low resolution, error, worst quality, low quality, defects", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"enable_prompt_expansion": ("BOOLEAN", {"default": True, "tooltip": "Let the model expand/rewrite the prompt for better results."}),
"multi_shots": ("BOOLEAN", {"default": True, "tooltip": "Allow multiple shots/cuts in the generated video."}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the content safety checker on the input."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, duration, image=None, audio_url="", aspect_ratio="16:9", resolution="1080p", negative_prompt="low resolution, error, worst quality, low quality, defects", enable_prompt_expansion=True, multi_shots=True, seed=-1, enable_safety_checker=True):
try:
# Conditional routing based on whether image is provided
if image is None:
# T2V mode: Use text-to-video endpoint
endpoint = "wan/v2.6/text-to-video"
arguments = {
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"negative_prompt": negative_prompt,
"enable_prompt_expansion": enable_prompt_expansion,
"multi_shots": multi_shots,
"enable_safety_checker": enable_safety_checker,
}
else:
# I2V mode: Use image-to-video endpoint
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"wan/v2.6", "Failed to upload image"
)
endpoint = "wan/v2.6/image-to-video"
arguments = {
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"resolution": resolution,
"negative_prompt": negative_prompt,
"enable_prompt_expansion": enable_prompt_expansion,
"multi_shots": multi_shots,
"enable_safety_checker": enable_safety_checker,
}
# Add optional audio URL if provided
if audio_url and audio_url.strip():
arguments["audio_url"] = audio_url.strip()
# Add seed if specified (not -1)
if seed != -1:
arguments["seed"] = seed
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"wan/v2.6", e
)
class FalWan26ReferenceToVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "Dance battle between @Video1 and @Video2.", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"video1_url": ("STRING", {"default": "", "tooltip": "URL of reference video 1, referenced as @Video1 in the prompt."}),
},
"optional": {
"video2_url": ("STRING", {"default": "", "tooltip": "URL of reference video 2, referenced as @Video2 in the prompt."}),
"video3_url": ("STRING", {"default": "", "tooltip": "URL of reference video 3, referenced as @Video3 in the prompt."}),
"aspect_ratio": (["16:9", "9:16", "1:1", "4:3", "3:4"], {"default": "16:9", "tooltip": "Aspect ratio of the generated video."}),
"resolution": (["720p", "1080p"], {"default": "1080p", "tooltip": "Output video resolution."}),
"duration": (["5", "10"], {"default": "5", "tooltip": "Length of the generated video in seconds."}),
"negative_prompt": ("STRING", {"default": "low resolution, error, worst quality, low quality, defects", "multiline": True, "tooltip": "Describes content and artifacts to avoid in the generated video."}),
"enable_prompt_expansion": ("BOOLEAN", {"default": True, "tooltip": "Let the model expand/rewrite the prompt for better results."}),
"multi_shots": ("BOOLEAN", {"default": True, "tooltip": "Allow multiple shots/cuts in the generated video."}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647, "tooltip": "Random seed for reproducible results; leave at the default to let the API choose."}),
"enable_safety_checker": ("BOOLEAN", {"default": True, "tooltip": "Run the content safety checker on the input."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, video1_url, video2_url="", video3_url="", aspect_ratio="16:9", resolution="1080p", duration="5", negative_prompt="low resolution, error, worst quality, low quality, defects", enable_prompt_expansion=True, multi_shots=True, seed=-1, enable_safety_checker=True):
try:
# Build video_urls list from provided URLs
video_urls = []
if video1_url and video1_url.strip():
video_urls.append(video1_url.strip())
if video2_url and video2_url.strip():
video_urls.append(video2_url.strip())
if video3_url and video3_url.strip():
video_urls.append(video3_url.strip())
if not video_urls:
return ApiHandler.handle_video_generation_error(
"wan/v2.6/reference-to-video", "At least one video URL is required"
)
arguments = {
"prompt": prompt,
"video_urls": video_urls,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"duration": duration,
"negative_prompt": negative_prompt,
"enable_prompt_expansion": enable_prompt_expansion,
"multi_shots": multi_shots,
"enable_safety_checker": enable_safety_checker,
}
# Add seed if specified (not -1)
if seed != -1:
arguments["seed"] = seed
result = ApiHandler.submit_and_get_result("wan/v2.6/reference-to-video", arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"wan/v2.6/reference-to-video", e
)
class FalSora2ProImageToVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"image": ("IMAGE",{"tooltip": "Input image, uploaded to fal and used as the source/start image for generation."}),
},
"optional": {
"resolution": (["auto", "720p", "1080p"], {"default": "auto", "tooltip": "Output video resolution."}),
"aspect_ratio": (["auto", "9:16", "16:9"], {"default": "auto", "tooltip": "Aspect ratio of the generated video."}),
"duration": ([4, 8, 12], {"default": 4, "tooltip": "Length of the generated video in seconds."}),
"delete_video": ("BOOLEAN", {"default": True, "tooltip": "Delete the generated video from the provider after retrieval."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, image, resolution="auto", aspect_ratio="auto", duration=4, delete_video=True):
try:
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_video_generation_error(
"sora-2/pro", "Failed to upload image"
)
arguments = {
"prompt": prompt,
"image_url": image_url,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
"duration": duration,
"delete_video": delete_video,
}
result = ApiHandler.submit_and_get_result(
"fal-ai/sora-2/image-to-video/pro", arguments
)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"sora-2/pro", e
)
class FalVeo31FirstLastFrameToVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"first_frame": ("IMAGE",{"tooltip": "Image used as the first frame of the video."}),
},
"optional": {
"last_frame": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"duration": (["4s", "6s", "8s"], {"default": "8s", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["auto", "9:16", "16:9", "1:1"], {"default": "auto", "tooltip": "Aspect ratio of the generated video."}),
"resolution": (["720p", "1080p"], {"default": "720p", "tooltip": "Output video resolution."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, first_frame, last_frame=None, duration="8s", aspect_ratio="auto", resolution="720p", generate_audio=True):
try:
first_frame_url = ImageUtils.upload_image(first_frame)
if not first_frame_url:
return ApiHandler.handle_video_generation_error(
"veo3.1", "Failed to upload first frame"
)
# Conditional routing based on whether last_frame is provided
if last_frame is None:
# Use image-to-video endpoint (first frame only)
endpoint = "fal-ai/veo3.1/image-to-video"
arguments = {
"prompt": prompt,
"image_url": first_frame_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"generate_audio": generate_audio,
}
else:
# Use first-last-frame-to-video endpoint (both frames)
endpoint = "fal-ai/veo3.1/first-last-frame-to-video"
last_frame_url = ImageUtils.upload_image(last_frame)
if not last_frame_url:
return ApiHandler.handle_video_generation_error(
"veo3.1", "Failed to upload last frame"
)
arguments = {
"prompt": prompt,
"first_frame_url": first_frame_url,
"last_frame_url": last_frame_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"generate_audio": generate_audio,
}
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"veo3.1", e
)
class FalVeo31FastFirstLastFrameToVideo:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Text prompt describing the desired video content and motion."}),
"first_frame": ("IMAGE",{"tooltip": "Image used as the first frame of the video."}),
},
"optional": {
"last_frame": ("IMAGE",{"tooltip": "Optional image used as the last frame of the video."}),
"duration": (["4s", "6s", "8s"], {"default": "8s", "tooltip": "Length of the generated video in seconds."}),
"aspect_ratio": (["auto", "9:16", "16:9", "1:1"], {"default": "auto", "tooltip": "Aspect ratio of the generated video."}),
"resolution": (["720p", "1080p"], {"default": "720p", "tooltip": "Output video resolution."}),
"generate_audio": ("BOOLEAN", {"default": True, "tooltip": "Also generate an audio track for the video."}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_video"
CATEGORY = "FAL/VideoGeneration"
def generate_video(self, prompt, first_frame, last_frame=None, duration="8s", aspect_ratio="auto", resolution="720p", generate_audio=True):
try:
first_frame_url = ImageUtils.upload_image(first_frame)
if not first_frame_url:
return ApiHandler.handle_video_generation_error(
"veo3.1/fast", "Failed to upload first frame"
)
# Conditional routing based on whether last_frame is provided
if last_frame is None:
# Use image-to-video endpoint (first frame only)
endpoint = "fal-ai/veo3.1/fast/image-to-video"
arguments = {
"prompt": prompt,
"image_url": first_frame_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"generate_audio": generate_audio,
}
else:
# Use first-last-frame-to-video endpoint (both frames)
endpoint = "fal-ai/veo3.1/fast/first-last-frame-to-video"
last_frame_url = ImageUtils.upload_image(last_frame)
if not last_frame_url:
return ApiHandler.handle_video_generation_error(
"veo3.1/fast", "Failed to upload last frame"
)
arguments = {
"prompt": prompt,
"first_frame_url": first_frame_url,
"last_frame_url": last_frame_url,
"duration": duration,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
"generate_audio": generate_audio,
}
result = ApiHandler.submit_and_get_result(endpoint, arguments)
video_url = result["video"]["url"]
return (video_url,)
except Exception as e:
return ApiHandler.handle_video_generation_error(
"veo3.1/fast", e
)
# Update Node class mappings
NODE_CLASS_MAPPINGS = {
"InfinityStarTextToVideo_fal": InfinityStarTextToVideoNode,
"Kling_fal": KlingNode,
"KlingPro10_fal": KlingPro10Node,
"KlingPro16_fal": KlingPro16Node,
"KlingMaster_fal": KlingMasterNode,
"KlingOmniImageToVideo_fal": KlingOmniImageToVideoNode,
"KlingOmniReferenceToVideo_fal": KlingOmniReferenceToVideoNode,
"KlingOmniVideoToVideoEdit_fal": KlingOmniVideoToVideoEditNode,
"KlingOmniVideoToVideoReference_fal": KlingOmniVideoToVideoReferenceNode,
"Krea_Wan14b_VideoToVideo_fal": KreaWan14bVideoToVideoNode,
"RunwayGen3_fal": RunwayGen3Node,
"LumaDreamMachine_fal": LumaDreamMachineNode,
"LoadVideoURL": LoadVideoURL,
"VideoFromURL_fal": VideoFromURL,
"UploadVideo_fal": UploadVideoNode,
"UploadFile_fal": UploadFileNode,
"MiniMax_fal": MiniMaxNode,
"MiniMaxTextToVideo_fal": MiniMaxTextToVideoNode,
"MiniMaxSubjectReference_fal": MiniMaxSubjectReferenceNode,
"PixverseSwapNode_fal": PixverseSwapNode,
"VideoUpscaler_fal": VideoUpscalerNode,
"CombinedVideoGeneration_fal": CombinedVideoGenerationNode,
"Veo2ImageToVideo_fal": Veo2ImageToVideoNode,
"WanPro_fal": WanProNode,
"Wan25_preview_fal": Wan25Node,
"WanVACEVideoEdit_fal": WanVACEVideoEditNode,
"Wan2214b_animate_replace_character_fal": Wan2214bAnimateReplaceNode,
"Wan2214b_animate_move_character_fal": Wan2214bAnimateMoveNode,
"Wan22VACEFun14b_fal": Wan22VACEFun14bNode,
"DYWanFun22_fal": DYWanFun22Node,
"DYWanUpscaler_fal": DYWanUpscalerNode,
"SeedanceImageToVideo_fal": SeedanceImageToVideoNode,
"SeedanceProImageToVideo_fal": SeedanceProImageToVideoNode,
"SeedanceTextToVideo_fal": SeedanceTextToVideoNode,
"Veo3_fal": Veo3Node,
"Kling21Pro_fal": FalKling21ProImageToVideo,
"Kling25TurboPro_fal": FalKling25TurboProImageToVideo,
"Kling26Pro_fal": FalKling26ProVideo,
"KlingV3Standard_fal": FalKlingV3StandardVideo,
"KlingV3Pro_fal": FalKlingV3ProVideo,
"KlingV3StandardMotionControl_fal": FalKlingV3StandardMotionControl,
"KlingV3ProMotionControl_fal": FalKlingV3ProMotionControl,
"KlingO3Standard_fal": FalKlingO3StandardVideo,
"KlingO3Pro_fal": FalKlingO3ProVideo,
"Wan26_fal": FalWan26Video,
"Wan26ReferenceToVideo_fal": FalWan26ReferenceToVideo,
"Sora2Pro_fal": FalSora2ProImageToVideo,
"Veo31_fal": FalVeo31FirstLastFrameToVideo,
"Veo31Fast_fal": FalVeo31FastFirstLastFrameToVideo,
}
# Update Node display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"InfinityStarTextToVideo_fal": "Infinity Star Text-to-Video (fal)",
"Kling_fal": "Kling Video Generation (fal)",
"KlingPro10_fal": "Kling Pro v1.0 Video Generation (fal)",
"KlingPro16_fal": "Kling Pro v1.6 Video Generation (fal)",
"KlingMaster_fal": "Kling Master v2.0 Video Generation (fal)",
"KlingOmniImageToVideo_fal": "Kling Omni Image-to-Video (fal)",
"KlingOmniReferenceToVideo_fal": "Kling Omni Reference-to-Video (fal)",
"KlingOmniVideoToVideoEdit_fal": "Kling Omni Video-to-Video Edit (fal)",
"KlingOmniVideoToVideoReference_fal": "Kling Omni Video-to-Video Reference (fal)",
"Krea_Wan14b_VideoToVideo_fal": "Krea Wan 14b Video-to-Video (fal)",
"RunwayGen3_fal": "Runway Gen3 Image-to-Video (fal)",
"LumaDreamMachine_fal": "Luma Dream Machine (fal)",
"LoadVideoURL": "Load Video from URL",
"VideoFromURL_fal": "Video from URL → VIDEO (fal)",
"UploadVideo_fal": "Upload Video (fal)",
"UploadFile_fal": "Upload File (fal)",
"MiniMax_fal": "MiniMax Video Generation (fal)",
"MiniMaxTextToVideo_fal": "MiniMax Text-to-Video (fal)",
"MiniMaxSubjectReference_fal": "MiniMax Subject Reference (fal)",
"PixverseSwapNode_fal": "Pixverse Swap (fal)",
"VideoUpscaler_fal": "Video Upscaler (fal)",
"CombinedVideoGeneration_fal": "Combined Video Generation (fal)",
"Veo2ImageToVideo_fal": "Google Veo2 Image-to-Video (fal)",
"WanPro_fal": "Wan Pro Image-to-Video (fal)",
"SeedanceImageToVideo_fal": "Seedance Image-to-Video (fal)",
"SeedanceProImageToVideo_fal": "Seedance Pro Image-to-Video (fal)",
"SeedanceTextToVideo_fal": "Seedance Text-to-Video (fal)",
"Veo3_fal": "Veo3 Video Generation (fal)",
"Wan25_preview_fal": "Wan 2.5 Preview Image-to-Video (fal)",
"WanVACEVideoEdit_fal": "Wan VACE Video Edit (fal)",
"Wan2214b_animate_replace_character_fal": "Wan 2.2 14b Animate: Replace Character (fal)",
"Wan2214b_animate_move_character_fal": "Wan 2.2 14b Animate: Move Character (fal)",
"Wan22VACEFun14b_fal": "Wan 2.2 VACE Fun 14b Video-to-Video (fal)",
"DYWanFun22_fal": "DY Wan Fun 22 Video Generation (fal)",
"DYWanUpscaler_fal": "DY Wan Upscaler (fal)",
"Kling21Pro_fal": "Kling v2.1 Pro Image-to-Video (fal)",
"Kling25TurboPro_fal": "Kling v2.5 Turbo Pro Image-to-Video (fal)",
"Kling26Pro_fal": "Kling v2.6 Pro Video Generation (fal)",
"KlingV3Standard_fal": "Kling V3 Standard Video Generation (fal)",
"KlingV3Pro_fal": "Kling V3 Pro Video Generation (fal)",
"KlingV3StandardMotionControl_fal": "Kling V3 Standard Motion Control (fal)",
"KlingV3ProMotionControl_fal": "Kling V3 Pro Motion Control (fal)",
"KlingO3Standard_fal": "Kling O3 Standard Video Generation (fal)",
"KlingO3Pro_fal": "Kling O3 Pro Video Generation (fal)",
"Wan26_fal": "Wan 2.6 Video Generation (fal)",
"Wan26ReferenceToVideo_fal": "Wan 2.6 Reference-to-Video (fal)",
"Sora2Pro_fal": "Sora 2 Pro Image-to-Video (fal)",
"Veo31_fal": "Veo 3.1 First-Last-Frame-to-Video (fal)",
"Veo31Fast_fal": "Veo 3.1 Fast First-Last-Frame-to-Video (fal)",
}