Compare commits
26
Commits
sync-client
...
pr-21
@@ -0,0 +1,28 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'gokayfem' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -160,3 +160,11 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Cursor and SpecStory
|
||||
.specstory/
|
||||
.cursor/
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
memory-bank/
|
||||
.DS_Store
|
||||
|
||||
@@ -56,14 +56,35 @@ After installation and configuration, restart ComfyUI. The new nodes will be ava
|
||||
- **Flux Dev (fal)**: Use the development version of Flux for image generation
|
||||
- **Flux Schnell (fal)**: Fast image generation with Flux Schnell
|
||||
- **Flux Pro 1.1 (fal)**: Latest version of Flux Pro for image generation
|
||||
- **Flux Ultra (fal)**: Ultra-high quality image generation with advanced controls
|
||||
- **Flux General (fal)**: ControlNets, Ipadapters, Loras for Flux Dev
|
||||
- **Flux LoRA (fal)**: Flux with dual LoRA support for custom styles
|
||||
- **Flux Pro Kontext (fal)**: Context-aware single image-to-image generation with max_quality toggle
|
||||
- **Flux Pro Kontext Multi (fal)**: Multi-image composition (2-4 images) with context awareness and max_quality toggle
|
||||
- **Flux Pro Kontext Text-to-Image (fal)**: Text-to-image with aspect ratio controls and max_quality toggle
|
||||
- **Recraft V3 (fal)**: Professional design generation with multiple style options
|
||||
- **Sana (fal)**: High-quality image synthesis with ultra-high resolution support
|
||||
- **HiDream Full (fal)**: Advanced image generation with comprehensive parameter control
|
||||
- **Ideogram v3 (fal)**: Advanced text-to-image generation with typography support
|
||||
|
||||
### Video Generation
|
||||
|
||||
- **Kling Video Generation (fal)**: Generate videos using the Kling model
|
||||
- **Kling Pro Video Generation (fal)**: Advanced video generation with Kling Pro
|
||||
- **Kling Pro v1.0 Video Generation (fal)**: Original version of Kling Pro for video generation
|
||||
- **Kling Pro v1.6 Video Generation (fal)**: Latest version of Kling Pro with improved quality
|
||||
- **Kling Master v2.0 Video Generation (fal)**: Advanced video generation with Kling Master
|
||||
- **Runway Gen3 Image-to-Video (fal)**: Convert images to videos using Runway Gen3
|
||||
- **Luma Dream Machine (fal)**: Create videos with Luma Dream Machine
|
||||
- **MiniMax Video Generation (fal)**: Generate videos using MiniMax model
|
||||
- **MiniMax Text-to-Video (fal)**: Create videos from text prompts using MiniMax
|
||||
- **MiniMax Subject Reference (fal)**: Generate videos with subject reference using MiniMax
|
||||
- **Google Veo2 Image-to-Video (fal)**: Convert images to videos using Google's Veo2 model
|
||||
- **Wan Pro Image-to-Video (fal)**: High-quality video generation with Wan Pro model
|
||||
- **Video Upscaler (fal)**: Upscale video quality using AI
|
||||
- **Combined Video Generation (fal)**: Generate videos using multiple services simultaneously
|
||||
- Supports Kling Pro v1.6, Kling Master v2.0, MiniMax, Luma, Veo2, and Wan Pro
|
||||
- Each service can be individually enabled/disabled
|
||||
- Wan Pro runs with safety checker enabled and automatic seed selection
|
||||
- **Load Video from URL**: Load and process videos from a given URL
|
||||
|
||||
### Language Models (LLMs)
|
||||
@@ -101,7 +122,7 @@ If you encounter any errors during installation or usage, try the following:
|
||||
1. Ensure you have the latest version of ComfyUI installed
|
||||
2. Update this custom node package:
|
||||
```
|
||||
cd custom_nodes/ComfyUI-FLUX-fal-API
|
||||
cd custom_nodes/ComfyUI-fal-API
|
||||
git pull
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
{
|
||||
"id": "e3b1097d-0ba0-4fb1-b1f9-510d5a9ee4b1",
|
||||
"revision": 0,
|
||||
"last_node_id": 65,
|
||||
"last_link_id": 71,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 23,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
400,
|
||||
830
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
370
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
60,
|
||||
64
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.32",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1300,
|
||||
1880
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
370
|
||||
],
|
||||
"flags": {
|
||||
"collapsed": false
|
||||
},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 66
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.32"
|
||||
},
|
||||
"widgets_values": [
|
||||
"kontext-sample-t2i"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1310,
|
||||
1330
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
370
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 65
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.32"
|
||||
},
|
||||
"widgets_values": [
|
||||
"kontext-sample-i2i-multi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1300,
|
||||
830
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
370
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 22
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.32"
|
||||
},
|
||||
"widgets_values": [
|
||||
"kontext-sample-i2i"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"type": "FluxProKontextMulti_fal",
|
||||
"pos": [
|
||||
800,
|
||||
1330
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
430
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image_1",
|
||||
"type": "IMAGE",
|
||||
"link": 64
|
||||
},
|
||||
{
|
||||
"name": "image_2",
|
||||
"type": "IMAGE",
|
||||
"link": 70
|
||||
},
|
||||
{
|
||||
"name": "image_3",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "image_4",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
65
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "a-und-b/ComfyUI-fal-API",
|
||||
"ver": "ec8880895d86bb5c720e167585b559e03fcf9024",
|
||||
"Node name for S&R": "FluxProKontextMulti_fal"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
"1:1",
|
||||
false,
|
||||
3.5,
|
||||
1,
|
||||
"2",
|
||||
"jpeg",
|
||||
false,
|
||||
22,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
400,
|
||||
1350
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
370
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
70
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.32",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"type": "FluxProKontext_fal",
|
||||
"pos": [
|
||||
800,
|
||||
830
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
370
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 60
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
22
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "a-und-b/ComfyUI-fal-API",
|
||||
"ver": "975d555e29c2bec2e8ffa1aa33f5bdc686345024",
|
||||
"Node name for S&R": "FluxProKontext_fal"
|
||||
},
|
||||
"widgets_values": [
|
||||
"make it a 3d render",
|
||||
"1:1",
|
||||
false,
|
||||
1,
|
||||
1,
|
||||
"6",
|
||||
"png",
|
||||
false,
|
||||
41,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"type": "FluxProKontextTextToImage_fal",
|
||||
"pos": [
|
||||
800,
|
||||
1880
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
380
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
66
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "a-und-b/ComfyUI-fal-API",
|
||||
"ver": "ec8880895d86bb5c720e167585b559e03fcf9024",
|
||||
"Node name for S&R": "FluxProKontextTextToImage_fal"
|
||||
},
|
||||
"widgets_values": [
|
||||
"a childs drawing of grumpy cat",
|
||||
"1:1",
|
||||
false,
|
||||
3.5,
|
||||
1,
|
||||
"2",
|
||||
"jpeg",
|
||||
false,
|
||||
33,
|
||||
"fixed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
22,
|
||||
22,
|
||||
0,
|
||||
24,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
60,
|
||||
23,
|
||||
0,
|
||||
22,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
64,
|
||||
23,
|
||||
0,
|
||||
59,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
65,
|
||||
59,
|
||||
0,
|
||||
62,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
66,
|
||||
60,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
70,
|
||||
61,
|
||||
0,
|
||||
59,
|
||||
1,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.18.9"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
+284
-3
@@ -169,6 +169,98 @@ class Recraft:
|
||||
print(f"Error generating image with Recraft: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
class HidreamFull:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image_size": (["square_hd", "square", "portrait_4_3", "portrait_16_9", "landscape_4_3", "landscape_16_9", "custom"], {"default": "landscape_4_3"}),
|
||||
"width": ("INT", {"default": 1024, "min": 512, "max": 1440, "step": 32}),
|
||||
"height": ("INT", {"default": 768, "min": 512, "max": 1440, "step": 32}),
|
||||
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 20.0}),
|
||||
"num_images": ("INT", {"default": 1, "min": 1, "max": 10}),
|
||||
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2"}),
|
||||
},
|
||||
"optional": {
|
||||
"seed": ("INT", {"default": -1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "FAL/Image"
|
||||
|
||||
def generate_image(self, prompt, image_size, width, height, num_inference_steps, guidance_scale, num_images, safety_tolerance, seed=-1):
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"num_inference_steps": num_inference_steps,
|
||||
"guidance_scale": guidance_scale,
|
||||
"num_images": num_images,
|
||||
"safety_tolerance": safety_tolerance
|
||||
}
|
||||
if image_size == "custom":
|
||||
arguments["image_size"] = {"width": width, "height": height}
|
||||
else:
|
||||
arguments["image_size"] = image_size
|
||||
if seed != -1:
|
||||
arguments["seed"] = seed
|
||||
|
||||
try:
|
||||
handler = fal_client.submit("fal-ai/hidream-i1-full", arguments=arguments)
|
||||
result = handler.get()
|
||||
return self.process_result(result)
|
||||
except Exception as e:
|
||||
print(f"Error generating image with Hidream Full: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
class Ideogramv3:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image_size": (["square_hd", "square", "portrait_4_3", "portrait_16_9", "landscape_4_3", "landscape_16_9", "custom"], {"default": "landscape_4_3"}),
|
||||
"width": ("INT", {"default": 1024, "min": 512, "max": 1440, "step": 32}),
|
||||
"height": ("INT", {"default": 768, "min": 512, "max": 1440, "step": 32}),
|
||||
"num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 100}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 20.0}),
|
||||
"num_images": ("INT", {"default": 1, "min": 1, "max": 10}),
|
||||
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2"}),
|
||||
},
|
||||
"optional": {
|
||||
"seed": ("INT", {"default": -1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "FAL/Image"
|
||||
|
||||
def generate_image(self, prompt, image_size, width, height, num_inference_steps, guidance_scale, num_images, safety_tolerance, seed=-1):
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"num_inference_steps": num_inference_steps,
|
||||
"guidance_scale": guidance_scale,
|
||||
"num_images": num_images,
|
||||
"safety_tolerance": safety_tolerance
|
||||
}
|
||||
if image_size == "custom":
|
||||
arguments["image_size"] = {"width": width, "height": height}
|
||||
else:
|
||||
arguments["image_size"] = image_size
|
||||
if seed != -1:
|
||||
arguments["seed"] = seed
|
||||
|
||||
try:
|
||||
handler = fal_client.submit("fal-ai/ideogram/v3", arguments=arguments)
|
||||
result = handler.get()
|
||||
return self.process_result(result)
|
||||
except Exception as e:
|
||||
print(f"Error generating image with Ideogramv3: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
class FluxPro:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -449,7 +541,7 @@ class FluxLora:
|
||||
return self.process_result(result)
|
||||
except Exception as e:
|
||||
print(f"Error generating image with FluxLora: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
return self.create_blank_image()
|
||||
|
||||
class FluxGeneral:
|
||||
@classmethod
|
||||
@@ -631,6 +723,185 @@ class FluxGeneral:
|
||||
result = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
||||
return result
|
||||
|
||||
class FluxProKontext:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
"optional": {
|
||||
"aspect_ratio": ([None, "21:9", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "9:21"], {"default": None}),
|
||||
"max_quality": ("BOOLEAN", {"default": False}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1}),
|
||||
"num_images": ("INT", {"default": 1, "min": 1, "max": 4}),
|
||||
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2"}),
|
||||
"output_format": (["jpeg", "png"], {"default": "jpeg"}),
|
||||
"sync_mode": ("BOOLEAN", {"default": False}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 2**32-1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "FAL/Image"
|
||||
|
||||
def generate_image(self, prompt, image, aspect_ratio="1:1", max_quality=False, guidance_scale=3.5, num_images=1, safety_tolerance="2", output_format="jpeg", sync_mode=False, seed=0):
|
||||
# Upload the input image to get URL
|
||||
image_url = upload_image(image)
|
||||
if not image_url:
|
||||
model_name = "Flux Pro Kontext Max" if max_quality else "Flux Pro Kontext"
|
||||
print(f"Error: Failed to upload image for {model_name}")
|
||||
return self.create_blank_image()
|
||||
|
||||
# Dynamic endpoint selection based on max_quality toggle
|
||||
endpoint = "fal-ai/flux-pro/kontext/max" if max_quality else "fal-ai/flux-pro/kontext"
|
||||
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"image_url": image_url,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"guidance_scale": guidance_scale,
|
||||
"num_images": num_images,
|
||||
"safety_tolerance": safety_tolerance,
|
||||
"output_format": output_format,
|
||||
"sync_mode": sync_mode
|
||||
}
|
||||
|
||||
if seed > 0:
|
||||
arguments["seed"] = seed
|
||||
|
||||
try:
|
||||
handler = fal_client.submit(endpoint, arguments=arguments)
|
||||
result = handler.get()
|
||||
return self.process_result(result)
|
||||
except Exception as e:
|
||||
model_name = "Flux Pro Kontext Max" if max_quality else "Flux Pro Kontext"
|
||||
print(f"Error generating image with {model_name}: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
class FluxProKontextMulti:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image_1": ("IMAGE",),
|
||||
"image_2": ("IMAGE",),
|
||||
},
|
||||
"optional": {
|
||||
"image_3": ("IMAGE",),
|
||||
"image_4": ("IMAGE",),
|
||||
"aspect_ratio": ([None, "21:9", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "9:21"], {"default": None}),
|
||||
"max_quality": ("BOOLEAN", {"default": False}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1}),
|
||||
"num_images": ("INT", {"default": 1, "min": 1, "max": 4}),
|
||||
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2"}),
|
||||
"output_format": (["jpeg", "png"], {"default": "jpeg"}),
|
||||
"sync_mode": ("BOOLEAN", {"default": False}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 2**32-1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "FAL/Image"
|
||||
|
||||
def generate_image(self, prompt, image_1, image_2, image_3=None, image_4=None, aspect_ratio="1:1", max_quality=False, guidance_scale=3.5, num_images=1, safety_tolerance="2", output_format="jpeg", sync_mode=False, seed=0):
|
||||
# Upload all provided images
|
||||
image_urls = []
|
||||
|
||||
for i, img in enumerate([image_1, image_2, image_3, image_4], 1):
|
||||
if img is not None:
|
||||
url = upload_image(img)
|
||||
if url:
|
||||
image_urls.append(url)
|
||||
else:
|
||||
model_name = "Flux Pro Kontext Max Multi" if max_quality else "Flux Pro Kontext Multi"
|
||||
print(f"Error: Failed to upload image {i} for {model_name}")
|
||||
return self.create_blank_image()
|
||||
|
||||
if len(image_urls) < 2:
|
||||
model_name = "Flux Pro Kontext Max Multi" if max_quality else "Flux Pro Kontext Multi"
|
||||
print(f"Error: At least 2 images required for {model_name}")
|
||||
return self.create_blank_image()
|
||||
|
||||
# Dynamic endpoint selection based on max_quality toggle
|
||||
endpoint = "fal-ai/flux-pro/kontext/max/multi" if max_quality else "fal-ai/flux-pro/kontext/multi"
|
||||
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"image_urls": image_urls,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"guidance_scale": guidance_scale,
|
||||
"num_images": num_images,
|
||||
"safety_tolerance": safety_tolerance,
|
||||
"output_format": output_format,
|
||||
"sync_mode": sync_mode
|
||||
}
|
||||
|
||||
if seed > 0:
|
||||
arguments["seed"] = seed
|
||||
|
||||
try:
|
||||
handler = fal_client.submit(endpoint, arguments=arguments)
|
||||
result = handler.get()
|
||||
return self.process_result(result)
|
||||
except Exception as e:
|
||||
model_name = "Flux Pro Kontext Max Multi" if max_quality else "Flux Pro Kontext Multi"
|
||||
print(f"Error generating image with {model_name}: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
class FluxProKontextTextToImage:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
"optional": {
|
||||
"aspect_ratio": (["21:9", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "9:21"], {"default": "1:1"}),
|
||||
"max_quality": ("BOOLEAN", {"default": False}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1}),
|
||||
"num_images": ("INT", {"default": 1, "min": 1, "max": 4}),
|
||||
"safety_tolerance": (["1", "2", "3", "4", "5", "6"], {"default": "2"}),
|
||||
"output_format": (["jpeg", "png"], {"default": "jpeg"}),
|
||||
"sync_mode": ("BOOLEAN", {"default": False}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 2**32-1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "FAL/Image"
|
||||
|
||||
def generate_image(self, prompt, aspect_ratio="1:1", max_quality=False, guidance_scale=3.5, num_images=1, safety_tolerance="2", output_format="jpeg", sync_mode=False, seed=0):
|
||||
# Dynamic endpoint selection based on max_quality toggle
|
||||
endpoint = "fal-ai/flux-pro/kontext/max/text-to-image" if max_quality else "fal-ai/flux-pro/kontext/text-to-image"
|
||||
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"guidance_scale": guidance_scale,
|
||||
"num_images": num_images,
|
||||
"safety_tolerance": safety_tolerance,
|
||||
"output_format": output_format,
|
||||
"sync_mode": sync_mode
|
||||
}
|
||||
|
||||
if seed > 0:
|
||||
arguments["seed"] = seed
|
||||
|
||||
try:
|
||||
handler = fal_client.submit(endpoint, arguments=arguments)
|
||||
result = handler.get()
|
||||
return self.process_result(result)
|
||||
except Exception as e:
|
||||
model_name = "Flux Pro Kontext Max Text-to-Image" if max_quality else "Flux Pro Kontext Text-to-Image"
|
||||
print(f"Error generating image with {model_name}: {str(e)}")
|
||||
return self.create_blank_image()
|
||||
|
||||
# Common methods for all classes
|
||||
def process_result(self, result):
|
||||
images = []
|
||||
@@ -656,12 +927,14 @@ def create_blank_image(self):
|
||||
return (img_tensor,)
|
||||
|
||||
# Add common methods to all classes
|
||||
for cls in [FluxPro, FluxDev, FluxSchnell, FluxPro11, FluxUltra, FluxGeneral, FluxLora, Recraft, Sana]:
|
||||
for cls in [Ideogramv3,HidreamFull, FluxPro, FluxDev, FluxSchnell, FluxPro11, FluxUltra, FluxGeneral, FluxLora, Recraft, Sana, FluxProKontext, FluxProKontextMulti, FluxProKontextTextToImage]:
|
||||
cls.process_result = process_result
|
||||
cls.create_blank_image = create_blank_image
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Ideogramv3_fal":Ideogramv3,
|
||||
"Hidreamfull_fal": HidreamFull,
|
||||
"FluxPro_fal": FluxPro,
|
||||
"FluxDev_fal": FluxDev,
|
||||
"FluxSchnell_fal": FluxSchnell,
|
||||
@@ -671,10 +944,15 @@ NODE_CLASS_MAPPINGS = {
|
||||
"FluxLora_fal": FluxLora,
|
||||
"Recraft_fal": Recraft,
|
||||
"Sana_fal": Sana,
|
||||
"FluxProKontext_fal": FluxProKontext,
|
||||
"FluxProKontextMulti_fal": FluxProKontextMulti,
|
||||
"FluxProKontextTextToImage_fal": FluxProKontextTextToImage,
|
||||
}
|
||||
|
||||
# Node display name mappings
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Ideogramv3_fal": "Ideogramv3 (fal)",
|
||||
"Hidreamfull_fal": "HidreamFull (fal)",
|
||||
"FluxPro_fal": "Flux Pro (fal)",
|
||||
"FluxDev_fal": "Flux Dev (fal)",
|
||||
"FluxSchnell_fal": "Flux Schnell (fal)",
|
||||
@@ -683,5 +961,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FluxGeneral_fal": "Flux General (fal)",
|
||||
"FluxLora_fal": "Flux LoRA (fal)",
|
||||
"Recraft_fal": "Recraft V3 (fal)",
|
||||
"Sana_fal": "Sana (fal)"
|
||||
"Sana_fal": "Sana (fal)",
|
||||
"FluxProKontext_fal": "Flux Pro Kontext (fal)",
|
||||
"FluxProKontextMulti_fal": "Flux Pro Kontext Multi (fal)",
|
||||
"FluxProKontextTextToImage_fal": "Flux Pro Kontext Text-to-Image (fal)",
|
||||
}
|
||||
@@ -163,14 +163,143 @@ class HunyuanVideoLoraTrainerNode:
|
||||
print(f"Error during LoRA training: {str(e)}")
|
||||
return ("Error: Training failed.", "")
|
||||
|
||||
class WanLoraTrainerNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"training_data_url": ("STRING", {"default": ""}),
|
||||
"number_of_steps": ("INT", {"default": 400, "min": 5, "max": 10000, "step": 1}),
|
||||
"learning_rate": ("FLOAT", {"default": 0.0002, "min": 0.00001, "max": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"trigger_phrase": ("STRING", {"default": ""}),
|
||||
"auto_scale_input": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("lora_file_url",)
|
||||
FUNCTION = "train_lora"
|
||||
CATEGORY = "FAL/Training"
|
||||
|
||||
def train_lora(self, training_data_url, number_of_steps, learning_rate, trigger_phrase="", auto_scale_input=True):
|
||||
try:
|
||||
if not training_data_url:
|
||||
return ("Error: No training data URL provided.",)
|
||||
|
||||
# Prepare arguments for the API
|
||||
arguments = {
|
||||
"training_data_url": training_data_url,
|
||||
"number_of_steps": number_of_steps,
|
||||
"learning_rate": learning_rate,
|
||||
"auto_scale_input": auto_scale_input
|
||||
}
|
||||
|
||||
if trigger_phrase:
|
||||
arguments["trigger_phrase"] = trigger_phrase
|
||||
|
||||
# Submit training job
|
||||
handler = fal_client.submit("fal-ai/wan-trainer", arguments=arguments)
|
||||
result = handler.get()
|
||||
|
||||
lora_url = result["lora_file"]["url"]
|
||||
|
||||
return (lora_url,)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during LoRA training: {str(e)}")
|
||||
return ("Error: Training failed.",)
|
||||
|
||||
class LtxVideoTrainerNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"training_data_url": ("STRING", {"default": ""}),
|
||||
"rank": (["8", "16", "32", "64", "128"], {"default": "128"}),
|
||||
"number_of_steps": ("INT", {"default": 1000, "min": 100, "max": 10000, "step": 1}),
|
||||
"number_of_frames": ("INT", {"default": 81, "min": 1, "max": 1000}),
|
||||
"frame_rate": ("INT", {"default": 25, "min": 1, "max": 60}),
|
||||
"resolution": (["low", "medium", "high"], {"default": "medium"}),
|
||||
"aspect_ratio": (["16:9", "1:1", "9:16"], {"default": "1:1"}),
|
||||
"learning_rate": ("FLOAT", {"default": 0.0002, "min": 0.00001, "max": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"trigger_phrase": ("STRING", {"default": ""}),
|
||||
"auto_scale_input": ("BOOLEAN", {"default": False}),
|
||||
"split_input_into_scenes": ("BOOLEAN", {"default": True}),
|
||||
"split_input_duration_threshold": ("FLOAT", {"default": 30.0, "min": 1.0, "max": 300.0}),
|
||||
"validation_negative_prompt": ("STRING", {"default": "blurry, low quality, bad quality, out of focus"}),
|
||||
"validation_number_of_frames": ("INT", {"default": 81, "min": 1, "max": 1000}),
|
||||
"validation_resolution": (["low", "medium", "high"], {"default": "high"}),
|
||||
"validation_aspect_ratio": (["16:9", "1:1", "9:16"], {"default": "1:1"}),
|
||||
"validation_reverse": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("lora_file_url",)
|
||||
FUNCTION = "train_lora"
|
||||
CATEGORY = "FAL/Training"
|
||||
|
||||
def train_lora(self, training_data_url, rank, number_of_steps, number_of_frames, frame_rate,
|
||||
resolution, aspect_ratio, learning_rate, trigger_phrase="", auto_scale_input=False,
|
||||
split_input_into_scenes=True, split_input_duration_threshold=30.0,
|
||||
validation_negative_prompt="blurry, low quality, bad quality, out of focus",
|
||||
validation_number_of_frames=81, validation_resolution="high",
|
||||
validation_aspect_ratio="1:1", validation_reverse=False):
|
||||
try:
|
||||
if not training_data_url:
|
||||
return ("Error: No training data URL provided.",)
|
||||
|
||||
# Prepare arguments for the API
|
||||
arguments = {
|
||||
"training_data_url": training_data_url,
|
||||
"rank": int(rank),
|
||||
"number_of_steps": number_of_steps,
|
||||
"number_of_frames": number_of_frames,
|
||||
"frame_rate": frame_rate,
|
||||
"resolution": resolution,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"learning_rate": learning_rate,
|
||||
"auto_scale_input": auto_scale_input,
|
||||
"split_input_into_scenes": split_input_into_scenes,
|
||||
"split_input_duration_threshold": split_input_duration_threshold,
|
||||
"validation_negative_prompt": validation_negative_prompt,
|
||||
"validation_number_of_frames": validation_number_of_frames,
|
||||
"validation_resolution": validation_resolution,
|
||||
"validation_aspect_ratio": validation_aspect_ratio,
|
||||
"validation_reverse": validation_reverse
|
||||
}
|
||||
|
||||
if trigger_phrase:
|
||||
arguments["trigger_phrase"] = trigger_phrase
|
||||
|
||||
# Submit training job
|
||||
handler = fal_client.submit("fal-ai/ltx-video-trainer", arguments=arguments)
|
||||
result = handler.get()
|
||||
|
||||
lora_url = result["lora_file"]["url"]
|
||||
|
||||
return (lora_url,)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during LoRA training: {str(e)}")
|
||||
return ("Error: Training failed.",)
|
||||
|
||||
# Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FluxLoraTrainer_fal": FluxLoraTrainerNode,
|
||||
"HunyuanVideoLoraTrainer_fal": HunyuanVideoLoraTrainerNode,
|
||||
"WanLoraTrainer_fal": WanLoraTrainerNode,
|
||||
"LtxVideoTrainer_fal": LtxVideoTrainerNode,
|
||||
}
|
||||
|
||||
# Node display name mappings
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FluxLoraTrainer_fal": "Flux LoRA Trainer (fal)",
|
||||
"HunyuanVideoLoraTrainer_fal": "Hunyuan Video LoRA Trainer (fal)",
|
||||
"WanLoraTrainer_fal": "WAN LoRA Trainer (fal)",
|
||||
"LtxVideoTrainer_fal": "LTX Video LoRA Trainer (fal)",
|
||||
}
|
||||
+403
-10
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import configparser
|
||||
from fal_client import submit, upload_file, AsyncClient
|
||||
import torch
|
||||
from PIL import Image
|
||||
import tempfile
|
||||
@@ -7,6 +8,8 @@ import numpy as np
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
import cv2
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from fal_client.client import SyncClient
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -91,7 +94,7 @@ class MiniMaxNode:
|
||||
"image_url": image_url,
|
||||
}
|
||||
|
||||
handler = fal_client.submit("fal-ai/minimax-video/image-to-video", arguments=arguments)
|
||||
handler = fal_client.submit("fal-ai/minimax/video-01-live/image-to-video", arguments=arguments)
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
@@ -169,7 +172,113 @@ class KlingNode:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
class KlingProNode:
|
||||
class KlingPro10Node:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"duration": (["5", "10"], {"default": "5"}),
|
||||
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"tail_image": ("IMAGE",),
|
||||
},
|
||||
}
|
||||
|
||||
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 = 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 = upload_image(tail_image)
|
||||
if tail_image_url:
|
||||
arguments["tail_image_url"] = tail_image_url
|
||||
else:
|
||||
return ("Error: Unable to upload tail image.",)
|
||||
|
||||
handler = fal_client.submit("fal-ai/kling-video/v1/pro/image-to-video", arguments=arguments)
|
||||
else:
|
||||
return ("Error: Unable to upload image.",)
|
||||
else:
|
||||
handler = fal_client.submit("fal-ai/kling-video/v1/pro/text-to-video", arguments=arguments)
|
||||
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
except Exception as e:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
class KlingPro16Node:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"duration": (["5", "10"], {"default": "5"}),
|
||||
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"tail_image": ("IMAGE",),
|
||||
},
|
||||
}
|
||||
|
||||
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 = 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 = upload_image(tail_image)
|
||||
if tail_image_url:
|
||||
arguments["tail_image_url"] = tail_image_url
|
||||
else:
|
||||
return ("Error: Unable to upload tail image.",)
|
||||
|
||||
handler = fal_client.submit("fal-ai/kling-video/v1.6/pro/image-to-video", arguments=arguments)
|
||||
else:
|
||||
return ("Error: Unable to upload image.",)
|
||||
else:
|
||||
handler = fal_client.submit("fal-ai/kling-video/v1.6/pro/text-to-video", arguments=arguments)
|
||||
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
except Exception as e:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
class KlingMasterNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -199,11 +308,11 @@ class KlingProNode:
|
||||
image_url = upload_image(image)
|
||||
if image_url:
|
||||
arguments["image_url"] = image_url
|
||||
handler = fal_client.submit("fal-ai/kling-video/v1/pro/image-to-video", arguments=arguments)
|
||||
handler = fal_client.submit("fal-ai/kling-video/v2/master/image-to-video", arguments=arguments)
|
||||
else:
|
||||
return ("Error: Unable to upload image.",)
|
||||
else:
|
||||
handler = fal_client.submit("fal-ai/kling-video/v1/pro/text-to-video", arguments=arguments)
|
||||
handler = fal_client.submit("fal-ai/kling-video/v2/master/text-to-video", arguments=arguments)
|
||||
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
@@ -258,6 +367,7 @@ class LumaDreamMachineNode:
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"end_image": ("IMAGE",),
|
||||
"loop": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
}
|
||||
@@ -266,7 +376,7 @@ class LumaDreamMachineNode:
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "FAL/VideoGeneration"
|
||||
|
||||
def generate_video(self, prompt, mode, aspect_ratio, image=None, loop=False):
|
||||
def generate_video(self, prompt, mode, aspect_ratio, image=None, end_image=None, loop=False):
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
@@ -281,9 +391,17 @@ class LumaDreamMachineNode:
|
||||
if not image_url:
|
||||
return ("Error: Unable to upload image.",)
|
||||
arguments["image_url"] = image_url
|
||||
endpoint = "fal-ai/luma-dream-machine/image-to-video"
|
||||
|
||||
if end_image is not None:
|
||||
end_image_url = upload_image(end_image)
|
||||
if end_image_url:
|
||||
arguments["end_image_url"] = end_image_url
|
||||
else:
|
||||
return ("Error: Unable to upload end image.",)
|
||||
|
||||
endpoint = "fal-ai/luma-dream-machine/ray-2/image-to-video"
|
||||
else:
|
||||
endpoint = "fal-ai/luma-dream-machine"
|
||||
endpoint = "fal-ai/luma-dream-machine/ray-2"
|
||||
|
||||
handler = fal_client.submit(endpoint, arguments=arguments)
|
||||
result = handler.get()
|
||||
@@ -500,10 +618,279 @@ class Veo2ImageToVideoNode:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
class CombinedVideoGenerationNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image": ("IMAGE",),
|
||||
"kling_duration": (["5", "10"], {"default": "5"}),
|
||||
"kling_luma_aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9"}),
|
||||
"luma_loop": ("BOOLEAN", {"default": False}),
|
||||
"veo2_aspect_ratio": (["auto", "auto_prefer_portrait", "16:9", "9:16"], {"default": "auto"}),
|
||||
"veo2_duration": (["5s", "6s", "7s", "8s"], {"default": "5s"}),
|
||||
"enable_klingpro": ("BOOLEAN", {"default": True}),
|
||||
"enable_klingmaster": ("BOOLEAN", {"default": True}),
|
||||
"enable_minimax": ("BOOLEAN", {"default": True}),
|
||||
"enable_luma": ("BOOLEAN", {"default": True}),
|
||||
"enable_veo2": ("BOOLEAN", {"default": True}),
|
||||
"enable_wanpro": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
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:
|
||||
print(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:
|
||||
print(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:
|
||||
print(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:
|
||||
print(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:
|
||||
print(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:
|
||||
print(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_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:
|
||||
print(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 = 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:
|
||||
print(f"Error in combined video generation: {str(e)}")
|
||||
return ("Error: Unable to generate videos.",) * 6
|
||||
|
||||
class WanProNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
"optional": {
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
|
||||
"enable_safety_checker": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "FAL/VideoGeneration"
|
||||
|
||||
def generate_video(self, prompt, image, seed=0, enable_safety_checker=True):
|
||||
try:
|
||||
image_url = upload_image(image)
|
||||
if not image_url:
|
||||
return ("Error: Unable 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
|
||||
|
||||
handler = fal_client.submit("fal-ai/wan-pro/image-to-video", arguments=arguments)
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
except Exception as e:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
# Update Node class mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Kling_fal": KlingNode,
|
||||
"KlingPro_fal": KlingProNode,
|
||||
"KlingPro10_fal": KlingPro10Node,
|
||||
"KlingPro16_fal": KlingPro16Node,
|
||||
"KlingMaster_fal": KlingMasterNode,
|
||||
"RunwayGen3_fal": RunwayGen3Node,
|
||||
"LumaDreamMachine_fal": LumaDreamMachineNode,
|
||||
"LoadVideoURL": LoadVideoURL,
|
||||
@@ -511,13 +898,17 @@ NODE_CLASS_MAPPINGS = {
|
||||
"MiniMaxTextToVideo_fal": MiniMaxTextToVideoNode,
|
||||
"MiniMaxSubjectReference_fal": MiniMaxSubjectReferenceNode,
|
||||
"VideoUpscaler_fal": VideoUpscalerNode,
|
||||
"CombinedVideoGeneration_fal": CombinedVideoGenerationNode,
|
||||
"Veo2ImageToVideo_fal": Veo2ImageToVideoNode,
|
||||
"WanPro_fal": WanProNode,
|
||||
}
|
||||
|
||||
# Update Node display name mappings
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Kling_fal": "Kling Video Generation (fal)",
|
||||
"KlingPro_fal": "Kling Pro 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)",
|
||||
"RunwayGen3_fal": "Runway Gen3 Image-to-Video (fal)",
|
||||
"LumaDreamMachine_fal": "Luma Dream Machine (fal)",
|
||||
"LoadVideoURL": "Load Video from URL",
|
||||
@@ -525,5 +916,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MiniMaxTextToVideo_fal": "MiniMax Text-to-Video (fal)",
|
||||
"MiniMaxSubjectReference_fal": "MiniMax Subject Reference (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)",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[project]
|
||||
name = "fal-api"
|
||||
description = "Custom nodes for using fal API. Video generation with Kling, Runway, Luma. Image generation with Flux. LLMs and VLMs OpenAI, Claude, Llama and Gemini."
|
||||
version = "1.0.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["fal-client", "torch"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/gokayfem/ComfyUI-fal-API"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "gokayfem"
|
||||
DisplayName = "ComfyUI-fal-API"
|
||||
Icon = ""
|
||||
Reference in New Issue
Block a user