From 208aa49fe238807a1c8b252b0a54ac65bf1bec94 Mon Sep 17 00:00:00 2001 From: Matthias Georgi Date: Sun, 12 Jan 2025 12:21:16 +0100 Subject: [PATCH] added FAL nodes --- poetry.lock | 37 ++++- pyproject.toml | 4 + requirements.txt | 4 + scripts/gen_docs.py | 1 + scripts/node_metadata.py | 1 + src/nodetool/common/settings.py | 1 + src/nodetool/metadata/nodes.json | 198 +++++++++++++++++++----- src/nodetool/nodes/fal/__init__.py | 1 + src/nodetool/nodes/fal/fal_node.py | 55 +++++++ src/nodetool/nodes/fal/text_to_image.py | 64 ++++++++ web/src/api.ts | 11 +- web/src/stores/NodeMenuStore.ts | 2 + 12 files changed, 339 insertions(+), 40 deletions(-) create mode 100644 src/nodetool/nodes/fal/__init__.py create mode 100644 src/nodetool/nodes/fal/fal_node.py create mode 100644 src/nodetool/nodes/fal/text_to_image.py diff --git a/poetry.lock b/poetry.lock index 777d2054..1ae1f61c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1709,6 +1709,28 @@ files = [ [package.extras] testing = ["hatch", "pre-commit", "pytest", "tox"] +[[package]] +name = "fal-client" +version = "0.5.6" +description = "Python client for fal.ai" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "fal_client-0.5.6-py3-none-any.whl", hash = "sha256:631fd857a3c44753ee46a2eea1e7276471453aca58faac9c3702f744c7c84050"}, + {file = "fal_client-0.5.6.tar.gz", hash = "sha256:d3afc4b6250023d0ee8437ec504558231d3b106d7aabc12cda8c39883faddecb"}, +] + +[package.dependencies] +httpx = ">=0.21.0,<1" +httpx-sse = ">=0.4.0,<0.5" + +[package.extras] +dev = ["fal-client[docs,test]"] +docs = ["sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme"] +test = ["pillow", "pytest", "pytest-asyncio"] + [[package]] name = "fastapi" version = "0.115.5" @@ -2508,6 +2530,19 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "httpx-sse" +version = "0.4.0" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + [[package]] name = "huggingface-hub" version = "0.26.2" @@ -9663,4 +9698,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "ddc4cdc507818b8be6b26006171823b20e4c7d432d667a4df1f8ec2a8af69e15" +content-hash = "5b0ec2d6057bd3ded2e5b01b07088c620980e4f29585ee60a02d0c4695ecc77c" diff --git a/pyproject.toml b/pyproject.toml index e352e579..7f6c127f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ datasets = "3.1.0" python-docx = "1.1.2" diffusers = { extras = ["torch"], version = "0.31.0" } einops = "0.8.0" +fal-client = "0.5.6" fastapi = "0.115.5" feedparser = "6.0.11" filelock = "3.16.1" @@ -140,3 +141,6 @@ pytest-xdist = "*" [tool.poetry.scripts] nodetool = "nodetool.cli:cli" + +[tool.poetry.requires-plugins] +poetry-plugin-export = ">=1.8" diff --git a/requirements.txt b/requirements.txt index 19e84304..e5fd2e78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,9 +58,11 @@ einops==0.8.0 ; python_version >= "3.10" and python_version < "4.0" email-validator==2.2.0 ; python_version >= "3.10" and python_version < "4.0" et-xmlfile==2.0.0 ; python_version >= "3.10" and python_version < "4.0" exceptiongroup==1.2.2 ; python_version >= "3.10" and python_version < "3.11" +fal-client==0.5.6 ; python_version >= "3.10" and python_version < "4.0" fastapi-cli[standard]==0.0.7 ; python_version >= "3.10" and python_version < "4.0" fastapi==0.115.5 ; python_version >= "3.10" and python_version < "4.0" fastapi[all]==0.115.5 ; python_version >= "3.10" and python_version < "4.0" +feedparser==6.0.11 ; python_version >= "3.10" and python_version < "4.0" ffmpeg-python==0.2.0 ; python_version >= "3.10" and python_version < "4.0" filelock==3.16.1 ; python_version >= "3.10" and python_version < "4.0" flatbuffers==24.12.23 ; python_version >= "3.10" and python_version < "4.0" @@ -86,6 +88,7 @@ h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0" httpcore==1.0.7 ; python_version >= "3.10" and python_version < "4.0" httplib2==0.22.0 ; python_version >= "3.10" and python_version < "4.0" httptools==0.6.4 ; python_version >= "3.10" and python_version < "4.0" +httpx-sse==0.4.0 ; python_version >= "3.10" and python_version < "4.0" httpx==0.27.2 ; python_version >= "3.10" and python_version < "4.0" huggingface-hub==0.26.2 ; python_version >= "3.10" and python_version < "4.0" humanfriendly==10.0 ; python_version >= "3.10" and python_version < "4.0" @@ -257,6 +260,7 @@ selenium==4.26.1 ; python_version >= "3.10" and python_version < "4.0" sentence-transformers==3.3.0 ; python_version >= "3.10" and python_version < "4.0" sentencepiece==0.2.0 ; python_version >= "3.10" and python_version < "4.0" setuptools==75.1.0 ; python_version >= "3.10" and python_version < "4.0" +sgmllib3k==1.0.0 ; python_version >= "3.10" and python_version < "4.0" shellingham==1.5.4 ; python_version >= "3.10" and python_version < "4.0" six==1.17.0 ; python_version >= "3.10" and python_version < "4.0" sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0" diff --git a/scripts/gen_docs.py b/scripts/gen_docs.py index 7f3034fa..04ec91aa 100644 --- a/scripts/gen_docs.py +++ b/scripts/gen_docs.py @@ -320,6 +320,7 @@ def document_function( import nodetool.nodes.anthropic import nodetool.nodes.apple import nodetool.nodes.chroma + import nodetool.nodes.fal import nodetool.nodes.google import nodetool.nodes.huggingface import nodetool.nodes.nodetool diff --git a/scripts/node_metadata.py b/scripts/node_metadata.py index 087e1e71..7e59e3ae 100644 --- a/scripts/node_metadata.py +++ b/scripts/node_metadata.py @@ -7,6 +7,7 @@ import nodetool.nodes.anthropic import nodetool.nodes.chroma import nodetool.nodes.comfy +import nodetool.nodes.fal import nodetool.nodes.huggingface import nodetool.nodes.nodetool import nodetool.nodes.openai diff --git a/src/nodetool/common/settings.py b/src/nodetool/common/settings.py index f7566029..ef3d310a 100644 --- a/src/nodetool/common/settings.py +++ b/src/nodetool/common/settings.py @@ -36,6 +36,7 @@ class SecretsModel(BaseModel): ELEVENLABS_API_KEY: str | None = Field( default=None, description="ElevenLabs API key" ) + FAL_API_KEY: str | None = Field(default=None, description="FAL API key") def get_system_file_path(filename: str) -> Path: diff --git a/src/nodetool/metadata/nodes.json b/src/nodetool/metadata/nodes.json index 439f0b87..e6c38465 100644 --- a/src/nodetool/metadata/nodes.json +++ b/src/nodetool/metadata/nodes.json @@ -22380,10 +22380,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 856598, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 498397, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23422,10 +23422,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 860728, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 502411, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23475,10 +23475,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 860972, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 502649, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23528,10 +23528,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 861182, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 502856, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23581,10 +23581,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 861388, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 503076, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23634,10 +23634,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 861596, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 503289, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23687,10 +23687,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 861859, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 503541, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23740,10 +23740,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 862063, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 503744, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -23809,10 +23809,10 @@ "year": 2025, "month": 1, "day": 12, - "hour": 10, - "minute": 46, - "second": 11, - "microsecond": 862309, + "hour": 12, + "minute": 18, + "second": 29, + "microsecond": 504008, "tzinfo": "UTC", "utc_offset": 0.0 }, @@ -74899,6 +74899,132 @@ "recommended_models": [], "basic_fields": [] }, + { + "title": "Flux V 1 Pro", + "description": "FLUX1.1 [pro] is an enhanced version of FLUX.1 [pro], improved image generation capabilities, delivering superior composition, detail, and artistic fidelity compared to its predecessor.\n fal, text, image", + "namespace": "fal.text_to_image", + "node_type": "fal.text_to_image.FluxV1Pro", + "layout": "default", + "properties": [ + { + "name": "prompt", + "type": { + "type": "str", + "optional": false, + "values": null, + "type_args": [], + "type_name": null + }, + "default": "", + "title": "Prompt", + "description": "The prompt to generate an image from", + "min": null, + "max": null + } + ], + "outputs": [ + { + "type": { + "type": "image", + "optional": false, + "values": null, + "type_args": [], + "type_name": null + }, + "name": "output", + "stream": false + } + ], + "the_model_info": {}, + "recommended_models": [], + "basic_fields": [ + "prompt" + ] + }, + { + "title": "Flux V 1 Pro Ultra", + "description": "FLUX1.1 [ultra] is the latest and most advanced version of FLUX.1 [pro], \n featuring cutting-edge improvements in image generation, delivering unparalleled \n composition, detail, and artistic fidelity.", + "namespace": "fal.text_to_image", + "node_type": "fal.text_to_image.FluxV1ProUltra", + "layout": "default", + "properties": [ + { + "name": "prompt", + "type": { + "type": "str", + "optional": false, + "values": null, + "type_args": [], + "type_name": null + }, + "default": "", + "title": "Prompt", + "description": "The prompt to generate an image from", + "min": null, + "max": null + } + ], + "outputs": [ + { + "type": { + "type": "image", + "optional": false, + "values": null, + "type_args": [], + "type_name": null + }, + "name": "output", + "stream": false + } + ], + "the_model_info": {}, + "recommended_models": [], + "basic_fields": [ + "prompt" + ] + }, + { + "title": "Redux V 3", + "description": "REDUX3 is a cutting-edge image generation model that combines advanced AI technology with \n advanced image processing techniques to deliver stunning visual results.", + "namespace": "fal.text_to_image", + "node_type": "fal.text_to_image.ReduxV3", + "layout": "default", + "properties": [ + { + "name": "prompt", + "type": { + "type": "str", + "optional": false, + "values": null, + "type_args": [], + "type_name": null + }, + "default": "", + "title": "Prompt", + "description": "The prompt to generate an image from", + "min": null, + "max": null + } + ], + "outputs": [ + { + "type": { + "type": "image", + "optional": false, + "values": null, + "type_args": [], + "type_name": null + }, + "name": "output", + "stream": false + } + ], + "the_model_info": {}, + "recommended_models": [], + "basic_fields": [ + "prompt" + ] + }, { "title": "Text To Speech", "description": "Converts text to speech using OpenAI TTS models.\n audio, tts, text-to-speech, voice, synthesis\n\n Use cases:\n - Generate spoken content for videos or podcasts\n - Create voice-overs for presentations\n - Assist visually impaired users with text reading\n - Produce audio versions of written content", diff --git a/src/nodetool/nodes/fal/__init__.py b/src/nodetool/nodes/fal/__init__.py new file mode 100644 index 00000000..6e3e042a --- /dev/null +++ b/src/nodetool/nodes/fal/__init__.py @@ -0,0 +1 @@ +import nodetool.nodes.fal.text_to_image \ No newline at end of file diff --git a/src/nodetool/nodes/fal/fal_node.py b/src/nodetool/nodes/fal/fal_node.py new file mode 100644 index 00000000..f7ff4cf7 --- /dev/null +++ b/src/nodetool/nodes/fal/fal_node.py @@ -0,0 +1,55 @@ +import os +from typing import Dict, Any +from fal_client import AsyncClient +from nodetool.workflows.base_node import BaseNode +from nodetool.workflows.processing_context import ProcessingContext + +class FALNode(BaseNode): + """ + FAL Node for interacting with FAL AI services. + Provides methods to submit and handle API requests to FAL endpoints. + """ + + @classmethod + def is_visible(cls) -> bool: + return cls is not FALNode + + async def submit_request( + self, + context: ProcessingContext, + application: str, + arguments: Dict[str, Any], + ) -> Dict[str, Any]: + """ + Submit a request to a FAL AI endpoint and return the result. + + Args: + application (str): The path to the FAL model (e.g., "fal-ai/flux/dev/image-to-image") + arguments (Dict[str, Any]): The arguments to pass to the model + with_logs (bool, optional): Whether to include logs in the response. Defaults to True. + + Returns: + Dict[str, Any]: The result from the FAL API + """ + client = AsyncClient() + + if context.environment.get("FAL_API_KEY") is None: + raise ValueError("FAL_API_KEY is not set in the environment") + + os.environ["FAL_KEY"] = context.environment.get("FAL_API_KEY") # type: ignore + + handler = await client.submit( + application, + arguments=arguments, + ) + + # Process events if requested + async for event in handler.iter_events(with_logs=True): + # You might want to implement a proper logging system here + print(event) + + # Get the final result + result = await handler.get() + return result + + diff --git a/src/nodetool/nodes/fal/text_to_image.py b/src/nodetool/nodes/fal/text_to_image.py new file mode 100644 index 00000000..39a5a746 --- /dev/null +++ b/src/nodetool/nodes/fal/text_to_image.py @@ -0,0 +1,64 @@ +from enum import Enum +from pydantic import Field +from nodetool.metadata.types import ImageRef +from nodetool.nodes.fal.fal_node import FALNode +from nodetool.workflows.processing_context import ProcessingContext + + + +class FluxV1Pro(FALNode): + """ + FLUX1.1 [pro] is an enhanced version of FLUX.1 [pro], improved image generation capabilities, delivering superior composition, detail, and artistic fidelity compared to its predecessor. + fal, text, image + """ + + prompt: str = Field(default="", description="The prompt to generate an image from") + + async def process(self, context: ProcessingContext) -> ImageRef: + res = await self.submit_request( + context=context, + application="fal-ai/flux-pro/v1.1", + arguments={"prompt": self.prompt}, + ) + assert res["images"] is not None + assert len(res["images"]) > 0 + return ImageRef(uri=res["images"][0]["url"]) + + +class FluxV1ProUltra(FALNode): + """ + FLUX1.1 [ultra] is the latest and most advanced version of FLUX.1 [pro], + featuring cutting-edge improvements in image generation, delivering unparalleled + composition, detail, and artistic fidelity. + """ + + prompt: str = Field(default="", description="The prompt to generate an image from") + + async def process(self, context: ProcessingContext) -> ImageRef: + res = await self.submit_request( + context=context, + application="fal-ai/flux-pro/v1.1-ultra", + arguments={"prompt": self.prompt}, + ) + assert res["images"] is not None + assert len(res["images"]) > 0 + return ImageRef(uri=res["images"][0]["url"]) + + +class ReduxV3(FALNode): + """ + REDUX3 is a cutting-edge image generation model that combines advanced AI technology with + advanced image processing techniques to deliver stunning visual results. + """ + + prompt: str = Field(default="", description="The prompt to generate an image from") + + async def process(self, context: ProcessingContext) -> ImageRef: + res = await self.submit_request( + context=context, + application="fal-ai/recraft-v3", + arguments={"prompt": self.prompt}, + ) + assert res["images"] is not None + assert len(res["images"]) > 0 + return ImageRef(uri=res["images"][0]["url"]) diff --git a/web/src/api.ts b/web/src/api.ts index 01d32f3e..018e182b 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1241,9 +1241,9 @@ export interface components { * "month": 1, * "day": 12, * "hour": 10, - * "minute": 4, - * "second": 6, - * "microsecond": 766992, + * "minute": 55, + * "second": 12, + * "microsecond": 212654, * "tzinfo": "UTC", * "utc_offset": 0 * } @@ -3046,6 +3046,11 @@ export interface components { * @description ElevenLabs API key */ ELEVENLABS_API_KEY?: string | null; + /** + * Fal Api Key + * @description FAL API key + */ + FAL_API_KEY?: string | null; }; /** SettingsModel */ SettingsModel: { diff --git a/web/src/stores/NodeMenuStore.ts b/web/src/stores/NodeMenuStore.ts index 5d54a519..68025fd4 100644 --- a/web/src/stores/NodeMenuStore.ts +++ b/web/src/stores/NodeMenuStore.ts @@ -249,6 +249,8 @@ const useNodeMenuStore = create((set, get) => { !secrets.ELEVENLABS_API_KEY || secrets.ELEVENLABS_API_KEY.trim() === "" ); + case "fal": + return !secrets.FAL_API_KEY || secrets.FAL_API_KEY.trim() === ""; case "aime": return ( !secrets.AIME_API_KEY ||