Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9b21a5e93 | ||
|
|
fbee93b5b5 | ||
|
|
845b9d46c5 | ||
|
|
31572e6e45 | ||
|
|
b60c18d8a8 | ||
|
|
58c54acbce | ||
|
|
27580456ed | ||
|
|
a68c56134c | ||
|
|
cd9eb99568 | ||
|
|
ef774a511b | ||
|
|
66d4dcf54d | ||
|
|
a6d061c0eb | ||
|
|
34d3a8396e | ||
|
|
06a30a6f21 | ||
|
|
f4f486edb0 | ||
|
|
1f6f476679 | ||
|
|
1e561ac944 | ||
|
|
cf523888a7 | ||
|
|
93aa2cbc04 | ||
|
|
5be02175f3 | ||
|
|
4ff17aa6ef | ||
|
|
a6d29a2d4c | ||
|
|
4215edebf0 | ||
|
|
a5c55fd44b | ||
|
|
63cb3fcf0d | ||
|
|
e1faa49e25 | ||
|
|
c8c437b495 | ||
|
|
81f9031625 | ||
|
|
1c1be8ae31 | ||
|
|
cd5c9ef258 | ||
|
|
ec8880895d | ||
|
|
f8b1efa75d | ||
|
|
975d555e29 | ||
|
|
4988995bf7 | ||
|
|
ee026dd560 | ||
|
|
93d6ad2875 | ||
|
|
c22792a581 | ||
|
|
779e0b1028 | ||
|
|
2f7f43da45 | ||
|
|
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
|
||||
# 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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+28
-32
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+26
-61
@@ -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)",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,2 +1,3 @@
|
||||
fal-client
|
||||
torch
|
||||
torch
|
||||
opencv-python
|
||||
|
||||
Reference in New Issue
Block a user