Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5c55fd44b | ||
|
|
63cb3fcf0d | ||
|
|
e1faa49e25 | ||
|
|
c8c437b495 | ||
|
|
81f9031625 | ||
|
|
1c1be8ae31 | ||
|
|
cd5c9ef258 | ||
|
|
ec8880895d | ||
|
|
f8b1efa75d | ||
|
|
975d555e29 | ||
|
|
4988995bf7 | ||
|
|
ee026dd560 | ||
|
|
93d6ad2875 | ||
|
|
c22792a581 | ||
|
|
779e0b1028 | ||
|
|
a8bdb5bc6d | ||
|
|
c3fae085d6 | ||
|
|
1c67dda258 | ||
|
|
96b0cd0976 | ||
|
|
116bfbd4e0 | ||
|
|
2797366781 | ||
|
|
ebecad477a | ||
|
|
a46b9465e6 | ||
|
|
1d2ecc823e | ||
|
|
a8c202b045 | ||
|
|
e2a41cc5ff | ||
|
|
68328f8526 | ||
|
|
6a4b736773 | ||
|
|
cfd626541b |
@@ -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
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.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 Dev (fal)**: Use the development version of Flux for image generation
|
||||||
- **Flux Schnell (fal)**: Fast image generation with Flux Schnell
|
- **Flux Schnell (fal)**: Fast image generation with Flux Schnell
|
||||||
- **Flux Pro 1.1 (fal)**: Latest version of Flux Pro for image generation
|
- **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 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
|
### Video Generation
|
||||||
|
|
||||||
- **Kling Video Generation (fal)**: Generate videos using the Kling model
|
- **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
|
- **Runway Gen3 Image-to-Video (fal)**: Convert images to videos using Runway Gen3
|
||||||
- **Luma Dream Machine (fal)**: Create videos with Luma Dream Machine
|
- **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
|
- **Load Video from URL**: Load and process videos from a given URL
|
||||||
|
|
||||||
### Language Models (LLMs)
|
### 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
|
1. Ensure you have the latest version of ComfyUI installed
|
||||||
2. Update this custom node package:
|
2. Update this custom node package:
|
||||||
```
|
```
|
||||||
cd custom_nodes/ComfyUI-FLUX-fal-API
|
cd custom_nodes/ComfyUI-fal-API
|
||||||
git pull
|
git pull
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
{
|
||||||
|
"id": "80767774-d39b-4f73-a75a-3c1327f92316",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 94,
|
||||||
|
"last_link_id": 199,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"type": "LoadImage",
|
||||||
|
"pos": [
|
||||||
|
2148.41357421875,
|
||||||
|
-644.6304931640625
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
284.00726318359375,
|
||||||
|
497.4951477050781
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
198
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MASK",
|
||||||
|
"type": "MASK",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"image (24).png",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"type": "LoadImage",
|
||||||
|
"pos": [
|
||||||
|
1846.3321533203125,
|
||||||
|
-654.7257080078125
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
270,
|
||||||
|
510.1016845703125
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
197
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MASK",
|
||||||
|
"type": "MASK",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"image (25).png",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"type": "PreviewImage",
|
||||||
|
"pos": [
|
||||||
|
2907.259765625,
|
||||||
|
-712.0502319335938
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
660.7576293945312,
|
||||||
|
723.7353515625
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 199
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "PreviewImage"
|
||||||
|
},
|
||||||
|
"widgets_values": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"type": "FluxProKontextMulti_fal",
|
||||||
|
"pos": [
|
||||||
|
2462.2119140625,
|
||||||
|
-595.9410400390625
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
400,
|
||||||
|
364
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image_1",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 197
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "image_2",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 198
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "image_3",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "image_4",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
199
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "FluxProKontextMulti_fal"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"Woman wearing this backpack on her way to jungle",
|
||||||
|
"9:16",
|
||||||
|
false,
|
||||||
|
3.5,
|
||||||
|
1,
|
||||||
|
"2",
|
||||||
|
"jpeg",
|
||||||
|
false,
|
||||||
|
2075416510,
|
||||||
|
"randomize"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
197,
|
||||||
|
92,
|
||||||
|
0,
|
||||||
|
91,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
198,
|
||||||
|
93,
|
||||||
|
0,
|
||||||
|
91,
|
||||||
|
1,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
199,
|
||||||
|
91,
|
||||||
|
0,
|
||||||
|
94,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 0.863837598531476,
|
||||||
|
"offset": [
|
||||||
|
-1745.427153953069,
|
||||||
|
795.6355480141049
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frontendVersion": "1.18.10",
|
||||||
|
"ue_links": [],
|
||||||
|
"VHS_latentpreview": false,
|
||||||
|
"VHS_latentpreviewrate": 0,
|
||||||
|
"VHS_MetadataImage": true,
|
||||||
|
"VHS_KeepIntermediate": true
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"id": "80767774-d39b-4f73-a75a-3c1327f92316",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 97,
|
||||||
|
"last_link_id": 202,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"type": "LoadImage",
|
||||||
|
"pos": [
|
||||||
|
1846.3321533203125,
|
||||||
|
-654.7257080078125
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
270,
|
||||||
|
510.1016845703125
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MASK",
|
||||||
|
"type": "MASK",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"image (25).png",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"type": "LoadVideoURL",
|
||||||
|
"pos": [
|
||||||
|
2672.1376953125,
|
||||||
|
-645.724609375
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
270,
|
||||||
|
266
|
||||||
|
],
|
||||||
|
"flags": {
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"type": "STRING",
|
||||||
|
"widget": {
|
||||||
|
"name": "url"
|
||||||
|
},
|
||||||
|
"link": 201
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "frames",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
202
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frame_count",
|
||||||
|
"type": "INT",
|
||||||
|
"links": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "video_info",
|
||||||
|
"type": "VHS_VIDEOINFO",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "LoadVideoURL"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"https://example.com/video.mp4",
|
||||||
|
0,
|
||||||
|
"Disabled",
|
||||||
|
512,
|
||||||
|
512,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"type": "VHS_VideoCombine",
|
||||||
|
"pos": [
|
||||||
|
2995.218994140625,
|
||||||
|
-646.7775268554688
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
215.01171875,
|
||||||
|
670.6875
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 202
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "audio",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "AUDIO",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta_batch",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "VHS_BatchManager",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "VAE",
|
||||||
|
"link": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "Filenames",
|
||||||
|
"type": "VHS_FILENAMES",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VHS_VideoCombine"
|
||||||
|
},
|
||||||
|
"widgets_values": {
|
||||||
|
"frame_rate": 25,
|
||||||
|
"loop_count": 0,
|
||||||
|
"filename_prefix": "Veo2",
|
||||||
|
"format": "video/h265-mp4",
|
||||||
|
"pix_fmt": "yuv420p10le",
|
||||||
|
"crf": 22,
|
||||||
|
"save_metadata": false,
|
||||||
|
"pingpong": false,
|
||||||
|
"save_output": true,
|
||||||
|
"videopreview": {
|
||||||
|
"hidden": false,
|
||||||
|
"paused": false,
|
||||||
|
"params": {
|
||||||
|
"filename": "Veo2_00001.mp4",
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "output",
|
||||||
|
"format": "video/h265-mp4",
|
||||||
|
"frame_rate": 25,
|
||||||
|
"workflow": "Veo2_00001.png",
|
||||||
|
"fullpath": "D:\\ComfyUI_windows_portable\\ComfyUI\\output\\Veo2_00001.mp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"type": "Veo2ImageToVideo_fal",
|
||||||
|
"pos": [
|
||||||
|
2199.614990234375,
|
||||||
|
-652.0396118164062
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
400,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "STRING",
|
||||||
|
"type": "STRING",
|
||||||
|
"links": [
|
||||||
|
201
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Veo2ImageToVideo_fal"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"Woman slowly turning",
|
||||||
|
"auto",
|
||||||
|
"5s"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
92,
|
||||||
|
0,
|
||||||
|
95,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
201,
|
||||||
|
95,
|
||||||
|
0,
|
||||||
|
96,
|
||||||
|
0,
|
||||||
|
"STRING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
202,
|
||||||
|
96,
|
||||||
|
0,
|
||||||
|
97,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 0.863837598531476,
|
||||||
|
"offset": [
|
||||||
|
-1694.469448600796,
|
||||||
|
792.8487236845591
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frontendVersion": "1.18.10",
|
||||||
|
"ue_links": [],
|
||||||
|
"VHS_latentpreview": false,
|
||||||
|
"VHS_latentpreviewrate": 0,
|
||||||
|
"VHS_MetadataImage": true,
|
||||||
|
"VHS_KeepIntermediate": true
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
+302
-6
@@ -16,10 +16,25 @@ config = configparser.ConfigParser()
|
|||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fal_key = config['API']['FAL_KEY']
|
if os.environ.get("FAL_KEY") is not None:
|
||||||
os.environ["FAL_KEY"] = fal_key
|
print("FAL_KEY found in environment variables")
|
||||||
|
fal_key = os.environ["FAL_KEY"]
|
||||||
|
else:
|
||||||
|
print("FAL_KEY not found in environment variables")
|
||||||
|
fal_key = config['API']['FAL_KEY']
|
||||||
|
print("FAL_KEY found in config.ini")
|
||||||
|
os.environ["FAL_KEY"] = fal_key
|
||||||
|
print("FAL_KEY set in environment variables")
|
||||||
|
|
||||||
|
# Check if FAL key is the default placeholder
|
||||||
|
if fal_key == "<your_fal_api_key_here>":
|
||||||
|
print("WARNING: You are using the default FAL API key placeholder!")
|
||||||
|
print("Please set your actual FAL API key in either:")
|
||||||
|
print("1. The config.ini file under [API] section")
|
||||||
|
print("2. Or as an environment variable named FAL_KEY")
|
||||||
|
print("Get your API key from: https://fal.ai/dashboard/keys")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Error: FAL_KEY not found in config.ini")
|
print("Error: FAL_KEY not found in config.ini or environment variables")
|
||||||
|
|
||||||
# Create the client with API key
|
# Create the client with API key
|
||||||
fal_client = SyncClient(key=fal_key)
|
fal_client = SyncClient(key=fal_key)
|
||||||
@@ -169,6 +184,98 @@ class Recraft:
|
|||||||
print(f"Error generating image with Recraft: {str(e)}")
|
print(f"Error generating image with Recraft: {str(e)}")
|
||||||
return self.create_blank_image()
|
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:
|
class FluxPro:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -449,7 +556,7 @@ class FluxLora:
|
|||||||
return self.process_result(result)
|
return self.process_result(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error generating image with FluxLora: {str(e)}")
|
print(f"Error generating image with FluxLora: {str(e)}")
|
||||||
return self.create_blank_image()
|
return self.create_blank_image()
|
||||||
|
|
||||||
class FluxGeneral:
|
class FluxGeneral:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -631,6 +738,185 @@ class FluxGeneral:
|
|||||||
result = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
result = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
||||||
return result
|
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
|
# Common methods for all classes
|
||||||
def process_result(self, result):
|
def process_result(self, result):
|
||||||
images = []
|
images = []
|
||||||
@@ -656,12 +942,14 @@ def create_blank_image(self):
|
|||||||
return (img_tensor,)
|
return (img_tensor,)
|
||||||
|
|
||||||
# Add common methods to all classes
|
# 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.process_result = process_result
|
||||||
cls.create_blank_image = create_blank_image
|
cls.create_blank_image = create_blank_image
|
||||||
|
|
||||||
# Node class mappings
|
# Node class mappings
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"Ideogramv3_fal":Ideogramv3,
|
||||||
|
"Hidreamfull_fal": HidreamFull,
|
||||||
"FluxPro_fal": FluxPro,
|
"FluxPro_fal": FluxPro,
|
||||||
"FluxDev_fal": FluxDev,
|
"FluxDev_fal": FluxDev,
|
||||||
"FluxSchnell_fal": FluxSchnell,
|
"FluxSchnell_fal": FluxSchnell,
|
||||||
@@ -671,10 +959,15 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"FluxLora_fal": FluxLora,
|
"FluxLora_fal": FluxLora,
|
||||||
"Recraft_fal": Recraft,
|
"Recraft_fal": Recraft,
|
||||||
"Sana_fal": Sana,
|
"Sana_fal": Sana,
|
||||||
|
"FluxProKontext_fal": FluxProKontext,
|
||||||
|
"FluxProKontextMulti_fal": FluxProKontextMulti,
|
||||||
|
"FluxProKontextTextToImage_fal": FluxProKontextTextToImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Node display name mappings
|
# Node display name mappings
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"Ideogramv3_fal": "Ideogramv3 (fal)",
|
||||||
|
"Hidreamfull_fal": "HidreamFull (fal)",
|
||||||
"FluxPro_fal": "Flux Pro (fal)",
|
"FluxPro_fal": "Flux Pro (fal)",
|
||||||
"FluxDev_fal": "Flux Dev (fal)",
|
"FluxDev_fal": "Flux Dev (fal)",
|
||||||
"FluxSchnell_fal": "Flux Schnell (fal)",
|
"FluxSchnell_fal": "Flux Schnell (fal)",
|
||||||
@@ -683,5 +976,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"FluxGeneral_fal": "Flux General (fal)",
|
"FluxGeneral_fal": "Flux General (fal)",
|
||||||
"FluxLora_fal": "Flux LoRA (fal)",
|
"FluxLora_fal": "Flux LoRA (fal)",
|
||||||
"Recraft_fal": "Recraft V3 (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)",
|
||||||
}
|
}
|
||||||
+18
-3
@@ -10,10 +10,25 @@ config = configparser.ConfigParser()
|
|||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fal_key = config['API']['FAL_KEY']
|
if os.environ.get("FAL_KEY") is not None:
|
||||||
os.environ["FAL_KEY"] = fal_key
|
print("FAL_KEY found in environment variables")
|
||||||
|
fal_key = os.environ["FAL_KEY"]
|
||||||
|
else:
|
||||||
|
print("FAL_KEY not found in environment variables")
|
||||||
|
fal_key = config['API']['FAL_KEY']
|
||||||
|
print("FAL_KEY found in config.ini")
|
||||||
|
os.environ["FAL_KEY"] = fal_key
|
||||||
|
print("FAL_KEY set in environment variables")
|
||||||
|
|
||||||
|
# Check if FAL key is the default placeholder
|
||||||
|
if fal_key == "<your_fal_api_key_here>":
|
||||||
|
print("WARNING: You are using the default FAL API key placeholder!")
|
||||||
|
print("Please set your actual FAL API key in either:")
|
||||||
|
print("1. The config.ini file under [API] section")
|
||||||
|
print("2. Or as an environment variable named FAL_KEY")
|
||||||
|
print("Get your API key from: https://fal.ai/dashboard/keys")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Error: FAL_KEY not found in config.ini")
|
print("Error: FAL_KEY not found in config.ini or environment variables")
|
||||||
|
|
||||||
# Create the client with API key
|
# Create the client with API key
|
||||||
fal_client = SyncClient(key=fal_key)
|
fal_client = SyncClient(key=fal_key)
|
||||||
|
|||||||
+18
-3
@@ -14,10 +14,25 @@ config = configparser.ConfigParser()
|
|||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fal_key = config['API']['FAL_KEY']
|
if os.environ.get("FAL_KEY") is not None:
|
||||||
os.environ["FAL_KEY"] = fal_key
|
print("FAL_KEY found in environment variables")
|
||||||
|
fal_key = os.environ["FAL_KEY"]
|
||||||
|
else:
|
||||||
|
print("FAL_KEY not found in environment variables")
|
||||||
|
fal_key = config['API']['FAL_KEY']
|
||||||
|
print("FAL_KEY found in config.ini")
|
||||||
|
os.environ["FAL_KEY"] = fal_key
|
||||||
|
print("FAL_KEY set in environment variables")
|
||||||
|
|
||||||
|
# Check if FAL key is the default placeholder
|
||||||
|
if fal_key == "<your_fal_api_key_here>":
|
||||||
|
print("WARNING: You are using the default FAL API key placeholder!")
|
||||||
|
print("Please set your actual FAL API key in either:")
|
||||||
|
print("1. The config.ini file under [API] section")
|
||||||
|
print("2. Or as an environment variable named FAL_KEY")
|
||||||
|
print("Get your API key from: https://fal.ai/dashboard/keys")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Error: FAL_KEY not found in config.ini")
|
print("Error: FAL_KEY not found in config.ini or environment variables")
|
||||||
|
|
||||||
# Create the client with API key
|
# Create the client with API key
|
||||||
fal_client = SyncClient(key=fal_key)
|
fal_client = SyncClient(key=fal_key)
|
||||||
|
|||||||
+18
-3
@@ -16,10 +16,25 @@ config = configparser.ConfigParser()
|
|||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fal_key = config['API']['FAL_KEY']
|
if os.environ.get("FAL_KEY") is not None:
|
||||||
os.environ["FAL_KEY"] = fal_key
|
print("FAL_KEY found in environment variables")
|
||||||
|
fal_key = os.environ["FAL_KEY"]
|
||||||
|
else:
|
||||||
|
print("FAL_KEY not found in environment variables")
|
||||||
|
fal_key = config['API']['FAL_KEY']
|
||||||
|
print("FAL_KEY found in config.ini")
|
||||||
|
os.environ["FAL_KEY"] = fal_key
|
||||||
|
print("FAL_KEY set in environment variables")
|
||||||
|
|
||||||
|
# Check if FAL key is the default placeholder
|
||||||
|
if fal_key == "<your_fal_api_key_here>":
|
||||||
|
print("WARNING: You are using the default FAL API key placeholder!")
|
||||||
|
print("Please set your actual FAL API key in either:")
|
||||||
|
print("1. The config.ini file under [API] section")
|
||||||
|
print("2. Or as an environment variable named FAL_KEY")
|
||||||
|
print("Get your API key from: https://fal.ai/dashboard/keys")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Error: FAL_KEY not found in config.ini")
|
print("Error: FAL_KEY not found in config.ini or environment variables")
|
||||||
|
|
||||||
# Create the client with API key
|
# Create the client with API key
|
||||||
fal_client = SyncClient(key=fal_key)
|
fal_client = SyncClient(key=fal_key)
|
||||||
|
|||||||
+421
-15
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
|
from fal_client import submit, upload_file, AsyncClient
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -7,22 +8,37 @@ import numpy as np
|
|||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import cv2
|
import cv2
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
from fal_client.client import SyncClient
|
from fal_client.client import SyncClient
|
||||||
|
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
parent_dir = os.path.dirname(current_dir)
|
parent_dir = os.path.dirname(current_dir)
|
||||||
config_path = os.path.join(parent_dir, "config.ini")
|
config_path = os.path.join(parent_dir, "config.ini")
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
|
|
||||||
# First set environment variable
|
|
||||||
try:
|
try:
|
||||||
fal_key = config['API']['FAL_KEY']
|
if os.environ.get("FAL_KEY") is not None:
|
||||||
|
print("FAL_KEY found in environment variables")
|
||||||
os.environ["FAL_KEY"] = fal_key
|
fal_key = os.environ["FAL_KEY"]
|
||||||
|
else:
|
||||||
|
print("FAL_KEY not found in environment variables")
|
||||||
|
fal_key = config['API']['FAL_KEY']
|
||||||
|
print("FAL_KEY found in config.ini")
|
||||||
|
os.environ["FAL_KEY"] = fal_key
|
||||||
|
print("FAL_KEY set in environment variables")
|
||||||
|
|
||||||
|
# Check if FAL key is the default placeholder
|
||||||
|
if fal_key == "<your_fal_api_key_here>":
|
||||||
|
print("WARNING: You are using the default FAL API key placeholder!")
|
||||||
|
print("Please set your actual FAL API key in either:")
|
||||||
|
print("1. The config.ini file under [API] section")
|
||||||
|
print("2. Or as an environment variable named FAL_KEY")
|
||||||
|
print("Get your API key from: https://fal.ai/dashboard/keys")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Error: FAL_KEY not found in config.ini")
|
print("Error: FAL_KEY not found in config.ini or environment variables")
|
||||||
|
|
||||||
# Create the client with your API key
|
# Create the client with your API key
|
||||||
fal_client = SyncClient(key=fal_key)
|
fal_client = SyncClient(key=fal_key)
|
||||||
@@ -91,7 +107,7 @@ class MiniMaxNode:
|
|||||||
"image_url": image_url,
|
"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()
|
result = handler.get()
|
||||||
video_url = result["video"]["url"]
|
video_url = result["video"]["url"]
|
||||||
return (video_url,)
|
return (video_url,)
|
||||||
@@ -169,7 +185,113 @@ class KlingNode:
|
|||||||
print(f"Error generating video: {str(e)}")
|
print(f"Error generating video: {str(e)}")
|
||||||
return ("Error: Unable to generate video.",)
|
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
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
@@ -199,11 +321,11 @@ class KlingProNode:
|
|||||||
image_url = upload_image(image)
|
image_url = upload_image(image)
|
||||||
if image_url:
|
if image_url:
|
||||||
arguments["image_url"] = 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:
|
else:
|
||||||
return ("Error: Unable to upload image.",)
|
return ("Error: Unable to upload image.",)
|
||||||
else:
|
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()
|
result = handler.get()
|
||||||
video_url = result["video"]["url"]
|
video_url = result["video"]["url"]
|
||||||
@@ -258,6 +380,7 @@ class LumaDreamMachineNode:
|
|||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
|
"end_image": ("IMAGE",),
|
||||||
"loop": ("BOOLEAN", {"default": False}),
|
"loop": ("BOOLEAN", {"default": False}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -266,7 +389,7 @@ class LumaDreamMachineNode:
|
|||||||
FUNCTION = "generate_video"
|
FUNCTION = "generate_video"
|
||||||
CATEGORY = "FAL/VideoGeneration"
|
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 = {
|
arguments = {
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"aspect_ratio": aspect_ratio,
|
"aspect_ratio": aspect_ratio,
|
||||||
@@ -281,9 +404,17 @@ class LumaDreamMachineNode:
|
|||||||
if not image_url:
|
if not image_url:
|
||||||
return ("Error: Unable to upload image.",)
|
return ("Error: Unable to upload image.",)
|
||||||
arguments["image_url"] = image_url
|
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:
|
else:
|
||||||
endpoint = "fal-ai/luma-dream-machine"
|
endpoint = "fal-ai/luma-dream-machine/ray-2"
|
||||||
|
|
||||||
handler = fal_client.submit(endpoint, arguments=arguments)
|
handler = fal_client.submit(endpoint, arguments=arguments)
|
||||||
result = handler.get()
|
result = handler.get()
|
||||||
@@ -500,10 +631,279 @@ class Veo2ImageToVideoNode:
|
|||||||
print(f"Error generating video: {str(e)}")
|
print(f"Error generating video: {str(e)}")
|
||||||
return ("Error: Unable to generate video.",)
|
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
|
# Update Node class mappings
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"Kling_fal": KlingNode,
|
"Kling_fal": KlingNode,
|
||||||
"KlingPro_fal": KlingProNode,
|
"KlingPro10_fal": KlingPro10Node,
|
||||||
|
"KlingPro16_fal": KlingPro16Node,
|
||||||
|
"KlingMaster_fal": KlingMasterNode,
|
||||||
"RunwayGen3_fal": RunwayGen3Node,
|
"RunwayGen3_fal": RunwayGen3Node,
|
||||||
"LumaDreamMachine_fal": LumaDreamMachineNode,
|
"LumaDreamMachine_fal": LumaDreamMachineNode,
|
||||||
"LoadVideoURL": LoadVideoURL,
|
"LoadVideoURL": LoadVideoURL,
|
||||||
@@ -511,13 +911,17 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"MiniMaxTextToVideo_fal": MiniMaxTextToVideoNode,
|
"MiniMaxTextToVideo_fal": MiniMaxTextToVideoNode,
|
||||||
"MiniMaxSubjectReference_fal": MiniMaxSubjectReferenceNode,
|
"MiniMaxSubjectReference_fal": MiniMaxSubjectReferenceNode,
|
||||||
"VideoUpscaler_fal": VideoUpscalerNode,
|
"VideoUpscaler_fal": VideoUpscalerNode,
|
||||||
|
"CombinedVideoGeneration_fal": CombinedVideoGenerationNode,
|
||||||
"Veo2ImageToVideo_fal": Veo2ImageToVideoNode,
|
"Veo2ImageToVideo_fal": Veo2ImageToVideoNode,
|
||||||
|
"WanPro_fal": WanProNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update Node display name mappings
|
# Update Node display name mappings
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"Kling_fal": "Kling Video Generation (fal)",
|
"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)",
|
"RunwayGen3_fal": "Runway Gen3 Image-to-Video (fal)",
|
||||||
"LumaDreamMachine_fal": "Luma Dream Machine (fal)",
|
"LumaDreamMachine_fal": "Luma Dream Machine (fal)",
|
||||||
"LoadVideoURL": "Load Video from URL",
|
"LoadVideoURL": "Load Video from URL",
|
||||||
@@ -525,5 +929,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"MiniMaxTextToVideo_fal": "MiniMax Text-to-Video (fal)",
|
"MiniMaxTextToVideo_fal": "MiniMax Text-to-Video (fal)",
|
||||||
"MiniMaxSubjectReference_fal": "MiniMax Subject Reference (fal)",
|
"MiniMaxSubjectReference_fal": "MiniMax Subject Reference (fal)",
|
||||||
"VideoUpscaler_fal": "Video Upscaler (fal)",
|
"VideoUpscaler_fal": "Video Upscaler (fal)",
|
||||||
|
"CombinedVideoGeneration_fal": "Combined Video Generation (fal)",
|
||||||
"Veo2ImageToVideo_fal": "Google Veo2 Image-to-Video (fal)",
|
"Veo2ImageToVideo_fal": "Google Veo2 Image-to-Video (fal)",
|
||||||
}
|
"WanPro_fal": "Wan Pro Image-to-Video (fal)",
|
||||||
|
}
|
||||||
|
|||||||
+18
-3
@@ -14,10 +14,25 @@ config = configparser.ConfigParser()
|
|||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fal_key = config['API']['FAL_KEY']
|
if os.environ.get("FAL_KEY") is not None:
|
||||||
os.environ["FAL_KEY"] = fal_key
|
print("FAL_KEY found in environment variables")
|
||||||
|
fal_key = os.environ["FAL_KEY"]
|
||||||
|
else:
|
||||||
|
print("FAL_KEY not found in environment variables")
|
||||||
|
fal_key = config['API']['FAL_KEY']
|
||||||
|
print("FAL_KEY found in config.ini")
|
||||||
|
os.environ["FAL_KEY"] = fal_key
|
||||||
|
print("FAL_KEY set in environment variables")
|
||||||
|
|
||||||
|
# Check if FAL key is the default placeholder
|
||||||
|
if fal_key == "<your_fal_api_key_here>":
|
||||||
|
print("WARNING: You are using the default FAL API key placeholder!")
|
||||||
|
print("Please set your actual FAL API key in either:")
|
||||||
|
print("1. The config.ini file under [API] section")
|
||||||
|
print("2. Or as an environment variable named FAL_KEY")
|
||||||
|
print("Get your API key from: https://fal.ai/dashboard/keys")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Error: FAL_KEY not found in config.ini")
|
print("Error: FAL_KEY not found in config.ini or environment variables")
|
||||||
|
|
||||||
# Create the client with API key
|
# Create the client with API key
|
||||||
fal_client = SyncClient(key=fal_key)
|
fal_client = SyncClient(key=fal_key)
|
||||||
|
|||||||
@@ -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.1"
|
||||||
|
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