Compare commits

...
Author SHA1 Message Date
gökay aydoğan f9b21a5e93 change optional image number 2025-09-11 16:12:08 +03:00
Gökay Aydoğan fbee93b5b5 Update pyproject.toml 2025-09-11 15:55:31 +03:00
Gökay Aydoğan 845b9d46c5 Update requirements.txt 2025-09-05 11:34:43 +03:00
Gökay Aydoğan 31572e6e45 Merge pull request #39 from PierrunoYT/feature/add-qwen-image-edit
feat: add Qwen Image Edit node with parallel CFG support
2025-09-05 11:32:45 +03:00
Gökay Aydoğan b60c18d8a8 Merge pull request #40 from gokayfem/nano-banana
nano banana edit
2025-09-05 11:32:19 +03:00
gokayfem 58c54acbce nano banana edit 2025-09-05 11:31:40 +03:00
PierrunoYTandClaude 27580456ed feat: add Qwen Image Edit node with parallel CFG support
- Added QwenImageEdit class to nodes/image_node.py
- Supports image editing with text prompts using fal-ai/qwen-image-edit endpoint
- Features flexible image sizing, inference control, and acceleration options
- Includes safety checker, output format selection, and seed control
- Added to NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS
- Follows existing code patterns for consistency and error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 17:30:08 +02:00
Gökay Aydoğan a68c56134c Merge pull request #33 from jimlee2048/main
feat: support SeedEdit 3.0
2025-07-20 18:40:57 +03:00
Jim Lee cd9eb99568 feat: support SeedEdit 3.0 2025-07-20 22:55:01 +08:00
Gökay Aydoğan ef774a511b Update pyproject.toml 2025-06-20 13:38:43 +03:00
Gökay Aydoğan 66d4dcf54d Merge pull request #28 from KhDu/feature/adding_new_nodes
Added new fal model nodes (Veo3 +Seedance + Imagen4)
2025-06-20 13:38:28 +03:00
Gökay Aydoğan a6d061c0eb Update image_node.py 2025-06-20 13:37:07 +03:00
KhDu 34d3a8396e Reverted Flux Kontext Max to being a boolean switch 2025-06-19 21:04:41 +03:00
KhDu 06a30a6f21 Added Veo3 model 2025-06-19 18:29:55 +03:00
KhDu f4f486edb0 Added Kontext Multi, seperated Kontext Max into its own node.
Added Seedance Video model.

Added Imagen4 Image model.
2025-06-19 18:21:20 +03:00
KhDu 1f6f476679 added imagen4 text-to-image, and seedance image-to-video 2025-06-16 21:21:37 +03:00
Gökay Aydoğan 1e561ac944 Update pyproject.toml 2025-06-02 20:39:18 +03:00
Gökay Aydoğan cf523888a7 Merge pull request #26 from gokayfem/big-refactor-cleaning
fix: big refactor and cleaning
2025-06-02 20:38:59 +03:00
gokayfem 93aa2cbc04 fix: big refactor and cleaning 2025-06-02 19:03:45 +03:00
Gökay Aydoğan 5be02175f3 Update pyproject.toml 2025-06-01 14:35:30 +03:00
gokayfem 4ff17aa6ef Merge branch 'main' of https://github.com/gokayfem/ComfyUI-FLUX-fal-API 2025-06-01 14:23:34 +03:00
gokayfem a6d29a2d4c readme 2025-06-01 14:23:13 +03:00
Gökay Aydoğan 4215edebf0 Merge pull request #23 from gokayfem/api-key-setup
feat: api key setup
2025-06-01 14:15:50 +03:00
gokayfem a5c55fd44b api key setup 2025-06-01 14:14:29 +03:00
Gökay Aydoğan 63cb3fcf0d Update pyproject.toml 2025-05-31 02:35:57 +03:00
gokayfem e1faa49e25 new workflow examples 2025-05-31 02:35:01 +03:00
Gökay Aydoğan c8c437b495 Merge pull request #22 from gokayfem/pr-21
Add Kontext
2025-05-31 02:09:04 +03:00
gokayfem 81f9031625 add none 2025-05-31 02:06:14 +03:00
Holger Will 1c1be8ae31 fix: remove opencv dependency to avoid ComfyUI ecosystem conflicts 2025-05-30 22:38:47 +02:00
Holger Will cd5c9ef258 feat: add example workflow 2025-05-30 20:42:11 +02:00
Holger Will ec8880895d fix: add missing aspect_ratio parameter to Kontext image generation methods and fix text-to-image endpoints. 2025-05-30 20:12:34 +02:00
Holger Will f8b1efa75d fix: update endpoint paths for multi quality image generation in image_node.py 2025-05-30 17:51:40 +02:00
Holger Will 975d555e29 feat: add new Flux Pro Kontext image generation nodes and update README and requirements 2025-05-30 15:56:47 +02:00
Gökay Aydoğan 4988995bf7 Merge pull request #11 from ComfyNodePRs/pyproject
Add pyproject.toml for Custom Node Registry
2025-05-28 05:55:31 +03:00
Gökay Aydoğan ee026dd560 Merge pull request #12 from ComfyNodePRs/publish
Add Github Action for Publishing to Comfy Registry
2025-05-28 05:55:22 +03:00
Gökay Aydoğan 93d6ad2875 Update pyproject.toml 2025-05-28 05:55:07 +03:00
Gökay Aydoğan c22792a581 Merge pull request #18 from venturero/new_image_nodes_2
New image nodes 2
2025-05-28 05:40:07 +03:00
Gökay Aydoğan 779e0b1028 Merge pull request #20 from gokayfem/trainers
feat: add wan and ltx trainer
2025-05-28 05:39:01 +03:00
gokayfem 2f7f43da45 feat: add wan and ltx trainer 2025-05-28 05:38:19 +03:00
semiventurero a8bdb5bc6d image_node.py file updated with ideogramv3 2025-05-25 14:14:16 +03:00
semiventurero c3fae085d6 hidream and ideogram 2025-05-25 13:56:12 +03:00
Gökay Aydoğan 1c67dda258 Merge pull request #15 from pixelworldai/main
Updated: KlingMaster/WanPro/CombinedVideoGeneration
2025-05-08 16:36:53 +03:00
pixelworld AI 96b0cd0976 docs: Add Wan Pro 2025-05-07 21:35:55 -05:00
pixelworld AI 116bfbd4e0 feat: Add Wan Pro, update Luma and Minimax endpoints, add Luma end image support 2025-05-07 21:35:29 -05:00
pixelworld AI 2797366781 feat: enhance video generation capabilities
- Add Kling Pro v1.6 node with tail image support

- Rename original Kling Pro to v1.0 for clarity

- Add Kling Master v2.0 node

- Update Combined Video Generation node:

  - Add service toggles for each provider

  - Use Kling Pro v1.6 instead of v1.0

  - Add version numbers to output names

  - Maintain concurrent processing of enabled services

- Fix API key initialization in combined node

- Add proper error handling for disabled services
2025-05-06 21:15:06 -05:00
pixelworld AI ebecad477a docs: update video generation section with new nodes and versions
- Add Kling Pro v1.0 and v1.6 nodes

- Add Kling Master v2.0 node

- Add MiniMax nodes (standard, text-to-video, subject reference)

- Add Google Veo2 node

- Add Video Upscaler node

- Add Combined Video Generation node with service toggles

- Update node descriptions and version information
2025-05-06 21:14:59 -05:00
Jacob Garner a46b9465e6 Veo2 and multivid update 2025-04-28 10:12:57 -05:00
Jacob Garner 1d2ecc823e SyncClient 2025-04-28 10:12:52 -05:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") a8c202b045 chore(publish): Add Github Action for Publishing to Comfy Registry 2025-04-26 06:00:37 +00:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") e2a41cc5ff chore(pyproject): Add pyproject.toml for Custom Node Registry 2025-04-26 06:00:37 +00:00
Gökay Aydoğan 68328f8526 Merge pull request #10 from gokayfem/sync-client
Sync client
2025-04-24 14:54:44 +03:00
pixelworldai 6a4b736773 fixed duration/aspect ratio definitions 2025-04-11 19:20:26 -05:00
pixelworldai cfd626541b Update video_node.py
Added Combined Video Generation (fal), for simultaneous Kling Pro, Luma Dream, and Minimax image2video generation.
2025-04-11 17:53:43 -05:00
15 changed files with 3283 additions and 750 deletions
+28
View File
@@ -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 }}
+8
View File
@@ -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
+28 -2
View File
@@ -44,6 +44,11 @@ Custom nodes for using Flux models with fal API in ComfyUI with only one API Ke
FAL_KEY = your_actual_api_key
```
4. Alternatively, you can set the FAL_KEY environment variable:
```bash
export FAL_KEY=your_actual_api_key
```
## Usage
After installation and configuration, restart ComfyUI. The new nodes will be available in the node browser under the "FAL" category.
@@ -56,14 +61,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 +127,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
```
+12 -8
View File
@@ -1,12 +1,13 @@
import importlib.util
import importlib
import importlib.util
node_list = [
"image_node",
"video_node",
"llm_node",
"vlm_node",
"trainer_node",
"image_node",
"video_node",
"llm_node",
"vlm_node",
"trainer_node",
"upscaler_node",
]
NODE_CLASS_MAPPINGS = {}
@@ -16,7 +17,10 @@ for module_name in node_list:
imported_module = importlib.import_module(f".nodes.{module_name}", __name__)
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **imported_module.NODE_DISPLAY_NAME_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {
**NODE_DISPLAY_NAME_MAPPINGS,
**imported_module.NODE_DISPLAY_NAME_MAPPINGS,
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
@@ -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
}
+261
View File
@@ -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
}
+220
View File
@@ -0,0 +1,220 @@
import configparser
import io
import os
import tempfile
import numpy as np
import requests
import torch
from fal_client.client import SyncClient
from PIL import Image
class FalConfig:
"""Singleton class to handle FAL configuration and client setup."""
_instance = None
_client = None
_key = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(FalConfig, cls).__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
"""Initialize configuration and API key."""
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
config_path = os.path.join(parent_dir, "config.ini")
config = configparser.ConfigParser()
config.read(config_path)
try:
if os.environ.get("FAL_KEY") is not None:
print("FAL_KEY found in environment variables")
self._key = os.environ["FAL_KEY"]
else:
print("FAL_KEY not found in environment variables")
self._key = config["API"]["FAL_KEY"]
print("FAL_KEY found in config.ini")
os.environ["FAL_KEY"] = self._key
print("FAL_KEY set in environment variables")
# Check if FAL key is the default placeholder
if self._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:
print("Error: FAL_KEY not found in config.ini or environment variables")
def get_client(self):
"""Get or create the FAL client."""
if self._client is None:
self._client = SyncClient(key=self._key)
return self._client
def get_key(self):
"""Get the FAL API key."""
return self._key
class ImageUtils:
"""Utility functions for image processing."""
@staticmethod
def tensor_to_pil(image):
"""Convert image tensor to PIL Image."""
try:
# Convert the image tensor to a numpy array
if isinstance(image, torch.Tensor):
image_np = image.cpu().numpy()
else:
image_np = np.array(image)
# Ensure the image is in the correct format (H, W, C)
if image_np.ndim == 4:
image_np = image_np.squeeze(0) # Remove batch dimension if present
if image_np.ndim == 2:
image_np = np.stack([image_np] * 3, axis=-1) # Convert grayscale to RGB
elif image_np.shape[0] == 3:
image_np = np.transpose(
image_np, (1, 2, 0)
) # Change from (C, H, W) to (H, W, C)
# Normalize the image data to 0-255 range
if image_np.dtype == np.float32 or image_np.dtype == np.float64:
image_np = (image_np * 255).astype(np.uint8)
# Convert to PIL Image
return Image.fromarray(image_np)
except Exception as e:
print(f"Error converting tensor to PIL: {str(e)}")
return None
@staticmethod
def upload_image(image):
"""Upload image tensor to FAL and return URL."""
try:
pil_image = ImageUtils.tensor_to_pil(image)
if not pil_image:
return None
# Save the image to a temporary file
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
pil_image.save(temp_file, format="PNG")
temp_file_path = temp_file.name
# Upload the temporary file
client = FalConfig().get_client()
image_url = client.upload_file(temp_file_path)
return image_url
except Exception as e:
print(f"Error uploading image: {str(e)}")
return None
finally:
# Clean up the temporary file
if "temp_file_path" in locals():
os.unlink(temp_file_path)
@staticmethod
def mask_to_image(mask):
"""Convert mask tensor to image tensor."""
result = (
mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
.movedim(1, -1)
.expand(-1, -1, -1, 3)
)
return result
class ResultProcessor:
"""Utility functions for processing API results."""
@staticmethod
def process_image_result(result):
"""Process image generation result and return tensor."""
try:
images = []
for img_info in result["images"]:
img_url = img_info["url"]
img_response = requests.get(img_url)
img = Image.open(io.BytesIO(img_response.content))
img_array = np.array(img).astype(np.float32) / 255.0
images.append(img_array)
# Stack the images along a new first dimension
stacked_images = np.stack(images, axis=0)
# Convert to PyTorch tensor
img_tensor = torch.from_numpy(stacked_images)
return (img_tensor,)
except Exception as e:
print(f"Error processing image result: {str(e)}")
return ResultProcessor.create_blank_image()
@staticmethod
def process_single_image_result(result):
"""Process single image result and return tensor."""
try:
img_url = result["image"]["url"]
img_response = requests.get(img_url)
img = Image.open(io.BytesIO(img_response.content))
img_array = np.array(img).astype(np.float32) / 255.0
# Stack the images along a new first dimension
stacked_images = np.stack([img_array], axis=0)
# Convert to PyTorch tensor
img_tensor = torch.from_numpy(stacked_images)
return (img_tensor,)
except Exception as e:
print(f"Error processing single image result: {str(e)}")
return ResultProcessor.create_blank_image()
@staticmethod
def create_blank_image():
"""Create a blank black image tensor."""
blank_img = Image.new("RGB", (512, 512), color="black")
img_array = np.array(blank_img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_array)[None,]
return (img_tensor,)
class ApiHandler:
"""Utility functions for API interactions."""
@staticmethod
def submit_and_get_result(endpoint, arguments):
"""Submit job to FAL API and get result."""
try:
client = FalConfig().get_client()
handler = client.submit(endpoint, arguments=arguments)
return handler.get()
except Exception as e:
print(f"Error submitting to {endpoint}: {str(e)}")
raise e
@staticmethod
def handle_video_generation_error(model_name, error):
"""Handle video generation errors consistently."""
print(f"Error generating video with {model_name}: {str(error)}")
return ("Error: Unable to generate video.",)
@staticmethod
def handle_image_generation_error(model_name, error):
"""Handle image generation errors consistently."""
print(f"Error generating image with {model_name}: {str(error)}")
return ResultProcessor.create_blank_image()
@staticmethod
def handle_text_generation_error(model_name, error):
"""Handle text generation errors consistently."""
print(f"Error generating text with {model_name}: {str(error)}")
return ("Error: Unable to generate text.",)
+1187 -272
View File
File diff suppressed because it is too large Load Diff
+28 -32
View File
@@ -1,22 +1,8 @@
import os
import configparser
from fal_client.client import SyncClient
from .fal_utils import ApiHandler, FalConfig
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
config_path = os.path.join(parent_dir, "config.ini")
# Initialize FalConfig
fal_config = FalConfig()
config = configparser.ConfigParser()
config.read(config_path)
try:
fal_key = config['API']['FAL_KEY']
os.environ["FAL_KEY"] = fal_key
except KeyError:
print("Error: FAL_KEY not found in config.ini")
# Create the client with API key
fal_client = SyncClient(key=fal_key)
class LLMNode:
@classmethod
@@ -24,11 +10,22 @@ class LLMNode:
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True}),
"model": (["google/gemini-flash-1.5-8b", "anthropic/claude-3.5-sonnet", "anthropic/claude-3-haiku",
"google/gemini-pro-1.5", "google/gemini-flash-1.5", "meta-llama/llama-3.2-1b-instruct",
"meta-llama/llama-3.2-3b-instruct", "meta-llama/llama-3.1-8b-instruct",
"meta-llama/llama-3.1-70b-instruct", "openai/gpt-4o-mini", "openai/gpt-4o"],
{"default": "google/gemini-flash-1.5-8b"}),
"model": (
[
"google/gemini-flash-1.5-8b",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3-haiku",
"google/gemini-pro-1.5",
"google/gemini-flash-1.5",
"meta-llama/llama-3.2-1b-instruct",
"meta-llama/llama-3.2-3b-instruct",
"meta-llama/llama-3.1-8b-instruct",
"meta-llama/llama-3.1-70b-instruct",
"openai/gpt-4o-mini",
"openai/gpt-4o",
],
{"default": "google/gemini-flash-1.5-8b"},
),
"system_prompt": ("STRING", {"default": "", "multiline": True}),
},
}
@@ -38,19 +35,18 @@ class LLMNode:
CATEGORY = "FAL/LLM"
def generate_text(self, prompt, model, system_prompt):
arguments = {
"model": model,
"prompt": prompt,
"system_prompt": system_prompt,
}
try:
handler = fal_client.submit("fal-ai/any-llm", arguments=arguments)
result = handler.get()
arguments = {
"model": model,
"prompt": prompt,
"system_prompt": system_prompt,
}
result = ApiHandler.submit_and_get_result("fal-ai/any-llm", arguments)
return (result["output"],)
except Exception as e:
print(f"Error generating text with LLM: {str(e)}")
return ("Error: Unable to generate text.",)
return ApiHandler.handle_text_generation_error(model, str(e))
# Node class mappings
NODE_CLASS_MAPPINGS = {
+284 -72
View File
@@ -1,53 +1,52 @@
import os
import configparser
from fal_client.client import SyncClient
import tempfile
import zipfile
import torch
from PIL import Image
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
config_path = os.path.join(parent_dir, "config.ini")
from .fal_utils import ApiHandler, FalConfig
config = configparser.ConfigParser()
config.read(config_path)
# Initialize FalConfig
fal_config = FalConfig()
try:
fal_key = config['API']['FAL_KEY']
os.environ["FAL_KEY"] = fal_key
except KeyError:
print("Error: FAL_KEY not found in config.ini")
# Create the client with API key
fal_client = SyncClient(key=fal_key)
def create_zip_from_images(images):
"""Create a zip file from a list of images."""
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as temp_zip:
with zipfile.ZipFile(temp_zip, 'w') as zf:
for idx, img_tensor in enumerate(images):
# Convert tensor to PIL Image
if isinstance(img_tensor, torch.Tensor):
# Convert to numpy and scale to 0-255 range
img_np = (img_tensor.cpu().numpy() * 255).astype('uint8')
# Handle different tensor formats
if img_np.shape[0] == 3: # If in format (C, H, W)
img_np = img_np.transpose(1, 2, 0)
img = Image.fromarray(img_np)
else:
img = img_tensor
try:
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
with zipfile.ZipFile(temp_zip, "w") as zf:
for idx, img_tensor in enumerate(images):
# Convert tensor to PIL Image
if isinstance(img_tensor, torch.Tensor):
# Convert to numpy and scale to 0-255 range
img_np = (img_tensor.cpu().numpy() * 255).astype("uint8")
# Handle different tensor formats
if img_np.shape[0] == 3: # If in format (C, H, W)
img_np = img_np.transpose(1, 2, 0)
img = Image.fromarray(img_np)
else:
img = img_tensor
# Save image to temporary file
with tempfile.NamedTemporaryFile(
suffix=".png", delete=False
) as temp_img:
img.save(temp_img, format="PNG")
temp_img_path = temp_img.name
# Add to zip file
zf.write(temp_img_path, f"image_{idx}.png")
os.unlink(temp_img_path)
# Use fal_client.upload_file instead of ApiHandler.upload_file
client = FalConfig().get_client()
return client.upload_file(temp_zip.name)
except Exception as e:
return ApiHandler.handle_text_generation_error(
"flux-lora-fast-training", f"Failed to create zip file: {str(e)}"
)
# Save image to temporary file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_img:
img.save(temp_img, format='PNG')
temp_img_path = temp_img.name
# Add to zip file
zf.write(temp_img_path, f'image_{idx}.png')
os.unlink(temp_img_path)
return fal_client.upload_file(temp_zip.name)
class FluxLoraTrainerNode:
@classmethod
@@ -55,7 +54,10 @@ class FluxLoraTrainerNode:
return {
"required": {
"images": ("IMAGE",),
"steps": ("INT", {"default": 1000, "min": 100, "max": 10000, "step": 100}),
"steps": (
"INT",
{"default": 1000, "min": 100, "max": 10000, "step": 100},
),
"create_masks": ("BOOLEAN", {"default": True}),
"is_style": ("BOOLEAN", {"default": False}),
},
@@ -64,21 +66,34 @@ class FluxLoraTrainerNode:
"images_zip_url": ("STRING", {"default": ""}),
"is_input_format_already_preprocessed": ("BOOLEAN", {"default": False}),
"data_archive_format": ("STRING", {"default": ""}),
}
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("lora_file_url",)
FUNCTION = "train_lora"
CATEGORY = "FAL/Training"
def train_lora(self, images, steps, create_masks, is_style, trigger_word="", images_zip_url="",
is_input_format_already_preprocessed=False, data_archive_format=""):
def train_lora(
self,
images,
steps,
create_masks,
is_style,
trigger_word="",
images_zip_url="",
is_input_format_already_preprocessed=False,
data_archive_format="",
):
try:
# Use provided zip URL if available, otherwise create and upload zip file
images_url = images_zip_url if images_zip_url else create_zip_from_images(images)
images_url = (
images_zip_url if images_zip_url else create_zip_from_images(images)
)
if not images_url:
return ("Error: Unable to upload images.", "")
return ApiHandler.handle_text_generation_error(
"flux-lora-fast-training", "Failed to upload images"
)
# Prepare arguments for the API
arguments = {
@@ -88,24 +103,25 @@ class FluxLoraTrainerNode:
"is_style": is_style,
"is_input_format_already_preprocessed": is_input_format_already_preprocessed,
}
if trigger_word:
arguments["trigger_word"] = trigger_word
if data_archive_format:
arguments["data_archive_format"] = data_archive_format
# Submit training job
handler = fal_client.submit("fal-ai/flux-lora-fast-training", arguments=arguments)
result = handler.get()
result = ApiHandler.submit_and_get_result(
"fal-ai/flux-lora-fast-training", arguments
)
lora_url = result["diffusers_lora_file"]["url"]
return (lora_url, )
return (lora_url,)
except Exception as e:
print(f"Error during LoRA training: {str(e)}")
return ("Error: Training failed.", "")
return ApiHandler.handle_text_generation_error(
"flux-lora-fast-training", str(e)
)
class HunyuanVideoLoraTrainerNode:
@classmethod
@@ -113,64 +129,260 @@ class HunyuanVideoLoraTrainerNode:
return {
"required": {
"images": ("IMAGE",),
"steps": ("INT", {"default": 1000, "min": 100, "max": 10000, "step": 100}),
"steps": (
"INT",
{"default": 1000, "min": 100, "max": 10000, "step": 100},
),
},
"optional": {
"trigger_word": ("STRING", {"default": ""}),
"learning_rate": ("FLOAT", {"default": 0.0001, "min": 0.00001, "max": 0.01}),
"learning_rate": (
"FLOAT",
{"default": 0.0001, "min": 0.00001, "max": 0.01},
),
"do_caption": ("BOOLEAN", {"default": True}),
"images_zip_url": ("STRING", {"default": ""}),
"data_archive_format": ("STRING", {"default": ""}),
}
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("lora_file_url",)
FUNCTION = "train_lora"
CATEGORY = "FAL/Training"
def train_lora(self, images, steps, trigger_word="", learning_rate=0.0001, do_caption=True,
images_zip_url="", data_archive_format=""):
def train_lora(
self,
images,
steps,
trigger_word="",
learning_rate=0.0001,
do_caption=True,
images_zip_url="",
data_archive_format="",
):
try:
# Use provided zip URL if available, otherwise create and upload zip file
images_url = images_zip_url if images_zip_url else create_zip_from_images(images)
images_url = (
images_zip_url if images_zip_url else create_zip_from_images(images)
)
if not images_url:
return ("Error: Unable to upload images.", "")
return ApiHandler.handle_text_generation_error(
"hunyuan-video-lora-training", "Failed to upload images"
)
# Prepare arguments for the API
arguments = {
"images_data_url": images_url,
"steps": steps,
"learning_rate": learning_rate,
"do_caption": do_caption
"do_caption": do_caption,
}
if trigger_word:
arguments["trigger_word"] = trigger_word
if data_archive_format:
arguments["data_archive_format"] = data_archive_format
# Submit training job
handler = fal_client.submit("fal-ai/hunyuan-video-lora-training", arguments=arguments)
result = handler.get()
result = ApiHandler.submit_and_get_result(
"fal-ai/hunyuan-video-lora-training", arguments
)
lora_url = result["diffusers_lora_file"]["url"]
return (lora_url,)
except Exception as e:
print(f"Error during LoRA training: {str(e)}")
return ("Error: Training failed.", "")
return ApiHandler.handle_text_generation_error(
"hunyuan-video-lora-training", str(e)
)
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 ApiHandler.handle_text_generation_error(
"wan-trainer", "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
result = ApiHandler.submit_and_get_result("fal-ai/wan-trainer", arguments)
lora_url = result["lora_file"]["url"]
return (lora_url,)
except Exception as e:
return ApiHandler.handle_text_generation_error("wan-trainer", str(e))
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 ApiHandler.handle_text_generation_error(
"ltx-video-trainer", "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
result = ApiHandler.submit_and_get_result(
"fal-ai/ltx-video-trainer", arguments
)
lora_url = result["lora_file"]["url"]
return (lora_url,)
except Exception as e:
return ApiHandler.handle_text_generation_error("ltx-video-trainer", str(e))
# 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)",
}
+67 -110
View File
@@ -1,60 +1,8 @@
import os
import configparser
import tempfile
import requests
from PIL import Image
import io
import numpy as np
import torch
from fal_client.client import SyncClient
from .fal_utils import ApiHandler, FalConfig, ImageUtils, ResultProcessor
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
config_path = os.path.join(parent_dir, "config.ini")
# Initialize FalConfig
fal_config = FalConfig()
config = configparser.ConfigParser()
config.read(config_path)
try:
fal_key = config['API']['FAL_KEY']
os.environ["FAL_KEY"] = fal_key
except KeyError:
print("Error: FAL_KEY not found in config.ini")
# Create the client with API key
fal_client = SyncClient(key=fal_key)
def upload_image(image):
try:
if isinstance(image, torch.Tensor):
image_np = image.cpu().numpy()
else:
image_np = np.array(image)
if image_np.ndim == 4:
image_np = image_np.squeeze(0)
if image_np.ndim == 2:
image_np = np.stack([image_np] * 3, axis=-1)
elif image_np.shape[0] == 3:
image_np = np.transpose(image_np, (1, 2, 0))
if image_np.dtype == np.float32 or image_np.dtype == np.float64:
image_np = (image_np * 255).astype(np.uint8)
pil_image = Image.fromarray(image_np)
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
pil_image.save(temp_file, format="PNG")
temp_file_path = temp_file.name
image_url = fal_client.upload_file(temp_file_path)
return image_url
except Exception as e:
print(f"Error uploading image: {str(e)}")
return None
finally:
if 'temp_file_path' in locals():
os.unlink(temp_file_path)
class UpscalerNode:
@classmethod
@@ -62,74 +10,83 @@ class UpscalerNode:
return {
"required": {
"image": ("IMAGE",),
"upscale_factor": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.5}),
"negative_prompt": ("STRING", {"default": "(worst quality, low quality, normal quality:2)", "multiline": True}),
"creativity": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.05}),
"resemblance": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05}),
"guidance_scale": ("FLOAT", {"default": 4.0, "min": 1.0, "max": 20.0, "step": 0.5}),
"upscale_factor": (
"FLOAT",
{"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.5},
),
"negative_prompt": (
"STRING",
{
"default": "(worst quality, low quality, normal quality:2)",
"multiline": True,
},
),
"creativity": (
"FLOAT",
{"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.05},
),
"resemblance": (
"FLOAT",
{"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05},
),
"guidance_scale": (
"FLOAT",
{"default": 4.0, "min": 1.0, "max": 20.0, "step": 0.5},
),
"num_inference_steps": ("INT", {"default": 18, "min": 1, "max": 100}),
"enable_safety_checker": ("BOOLEAN", {"default": True}),
},
"optional": {
"seed": ("INT", {"default": -1}),
}
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_upscaled_image"
CATEGORY = "FAL/Image"
def generate_upscaled_image(self, image, upscale_factor, negative_prompt, creativity, resemblance, guidance_scale, num_inference_steps, enable_safety_checker, seed=-1):
image_url = upload_image(image)
if not image_url:
print("Failed to upload image for upscaling.")
return self.create_blank_image()
arguments = {
"image_url": image_url,
"prompt": "masterpiece, best quality, highres",
"upscale_factor": upscale_factor,
"negative_prompt": negative_prompt,
"creativity": creativity,
"resemblance": resemblance,
"guidance_scale": guidance_scale,
"num_inference_steps": num_inference_steps,
"enable_safety_checker": enable_safety_checker
}
if seed != -1:
arguments["seed"] = seed
def generate_upscaled_image(
self,
image,
upscale_factor,
negative_prompt,
creativity,
resemblance,
guidance_scale,
num_inference_steps,
enable_safety_checker,
seed=-1,
):
try:
handler = fal_client.submit("fal-ai/clarity-upscaler", arguments=arguments)
result = handler.get()
return self.process_result(result)
# Upload the image using ImageUtils
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_image_generation_error(
"clarity-upscaler", "Failed to upload image for upscaling"
)
arguments = {
"image_url": image_url,
"prompt": "masterpiece, best quality, highres",
"upscale_factor": upscale_factor,
"negative_prompt": negative_prompt,
"creativity": creativity,
"resemblance": resemblance,
"guidance_scale": guidance_scale,
"num_inference_steps": num_inference_steps,
"enable_safety_checker": enable_safety_checker,
}
if seed != -1:
arguments["seed"] = seed
result = ApiHandler.submit_and_get_result(
"fal-ai/clarity-upscaler", arguments
)
return ResultProcessor.process_image_result(result)
except Exception as e:
print(f"Error generating upscaled image: {str(e)}")
return self.create_blank_image()
return ApiHandler.handle_image_generation_error("clarity-upscaler", str(e))
def process_result(self, result):
try:
img_url = result["image"]["url"]
img_response = requests.get(img_url)
img = Image.open(io.BytesIO(img_response.content))
img_array = np.array(img).astype(np.float32) / 255.0
# Stack the images along a new first dimension
stacked_images = np.stack([img_array], axis=0)
# Convert to PyTorch tensor
img_tensor = torch.from_numpy(stacked_images)
return (img_tensor,)
except Exception as e:
print(f"Error processing result: {str(e)}")
return self.create_blank_image()
def create_blank_image(self):
blank_img = Image.new('RGB', (512, 512), color='black')
img_array = np.array(blank_img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_array)[None,]
return (img_tensor,)
# Node class mappings
NODE_CLASS_MAPPINGS = {
@@ -139,4 +96,4 @@ NODE_CLASS_MAPPINGS = {
# Node display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"Upscaler_fal": "Clarity Upscaler (fal)",
}
}
+901 -192
View File
File diff suppressed because it is too large Load Diff
+26 -61
View File
@@ -1,26 +1,8 @@
import os
import configparser
from fal_client.client import SyncClient
import torch
from PIL import Image
import tempfile
import numpy as np
from .fal_utils import ApiHandler, FalConfig, ImageUtils
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
config_path = os.path.join(parent_dir, "config.ini")
# Initialize FalConfig
fal_config = FalConfig()
config = configparser.ConfigParser()
config.read(config_path)
try:
fal_key = config['API']['FAL_KEY']
os.environ["FAL_KEY"] = fal_key
except KeyError:
print("Error: FAL_KEY not found in config.ini")
# Create the client with API key
fal_client = SyncClient(key=fal_key)
class VLMNode:
@classmethod
@@ -28,9 +10,17 @@ class VLMNode:
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True}),
"model": (["google/gemini-flash-1.5-8b", "anthropic/claude-3.5-sonnet", "anthropic/claude-3-haiku",
"google/gemini-pro-1.5", "google/gemini-flash-1.5", "openai/gpt-4o"],
{"default": "google/gemini-flash-1.5-8b"}),
"model": (
[
"google/gemini-flash-1.5-8b",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3-haiku",
"google/gemini-pro-1.5",
"google/gemini-flash-1.5",
"openai/gpt-4o",
],
{"default": "google/gemini-flash-1.5-8b"},
),
"system_prompt": ("STRING", {"default": "", "multiline": True}),
"image": ("IMAGE",),
},
@@ -42,34 +32,12 @@ class VLMNode:
def generate_text(self, prompt, model, system_prompt, image):
try:
# Convert the image tensor to a numpy array
if isinstance(image, torch.Tensor):
image_np = image.cpu().numpy()
else:
image_np = np.array(image)
# Ensure the image is in the correct format (H, W, C)
if image_np.ndim == 4:
image_np = image_np.squeeze(0) # Remove batch dimension if present
if image_np.ndim == 2:
image_np = np.stack([image_np] * 3, axis=-1) # Convert grayscale to RGB
elif image_np.shape[0] == 3:
image_np = np.transpose(image_np, (1, 2, 0)) # Change from (C, H, W) to (H, W, C)
# Normalize the image data to 0-255 range
if image_np.dtype == np.float32 or image_np.dtype == np.float64:
image_np = (image_np * 255).astype(np.uint8)
# Convert to PIL Image
pil_image = Image.fromarray(image_np)
# Save the image to a temporary file
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
pil_image.save(temp_file, format="PNG")
temp_file_path = temp_file.name
# Upload the temporary file
image_url = fal_client.upload_file(temp_file_path)
# Upload the image using ImageUtils
image_url = ImageUtils.upload_image(image)
if not image_url:
return ApiHandler.handle_text_generation_error(
model, "Failed to upload image"
)
arguments = {
"model": model,
@@ -78,16 +46,13 @@ class VLMNode:
"image_url": image_url,
}
handler = fal_client.submit("fal-ai/any-llm/vision", arguments=arguments)
result = handler.get()
result = ApiHandler.submit_and_get_result(
"fal-ai/any-llm/vision", arguments
)
return (result["output"],)
except Exception as e:
print(f"Error generating text with VLM: {str(e)}")
return ("Error: Unable to generate text.",)
finally:
# Clean up the temporary file
if 'temp_file_path' in locals():
os.unlink(temp_file_path)
return ApiHandler.handle_text_generation_error(model, str(e))
# Node class mappings
NODE_CLASS_MAPPINGS = {
@@ -97,4 +62,4 @@ NODE_CLASS_MAPPINGS = {
# Node display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"VLM_fal": "VLM (fal)",
}
}
+15
View File
@@ -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.5"
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 = ""
+2 -1
View File
@@ -1,2 +1,3 @@
fal-client
torch
torch
opencv-python