├── .gitignore
├── flowy
├── lib_omost
│ ├── __init__.py
│ ├── utils.py
│ ├── greedy_encode.py
│ └── canvas.py
├── api_nodes
│ ├── replicate
│ │ ├── requirements.txt
│ │ ├── __init__.py
│ │ ├── pyproject.toml
│ │ ├── LICENSE
│ │ ├── supported_models.json
│ │ ├── import_schemas.py
│ │ ├── README.md
│ │ ├── example_workflows
│ │ │ ├── bark_and_musicgen.json
│ │ │ ├── simple-llava.json
│ │ │ ├── simple-garment-try-on.json
│ │ │ ├── flux.json
│ │ │ ├── simple-llama3.json
│ │ │ └── llama3-405b.json
│ │ ├── replicate_bridge.py
│ │ ├── schema_to_node.py
│ │ └── schemas
│ │ │ ├── salesforce_blip.json
│ │ │ └── jingyunliang_swinir.json
│ ├── __init__.py
│ ├── hailuo.py
│ ├── fluxultra.py
│ ├── keling.py
│ ├── luma.py
│ ├── flux.py
│ ├── ideogram.py
│ ├── fluxdevlora.py
│ ├── recraft.py
│ ├── clarityupscaler.py
│ └── base.py
├── api_key_manager.py
├── nodes_previewvideo.py
├── utils.py
├── nodes_llm.py
├── nodes_http.py
├── types.py
├── nodes.py
└── nodes_json.py
├── requirements.txt
├── makefile
├── images
├── LLM.png
├── Logo.png
├── API_Key.png
├── clarity.png
├── Omost_LLM.png
├── API_Key_Node.png
└── comflowy_banner.png
├── __init__.py
├── nodes_test.py
├── README_CN.md
├── web
└── js
│ └── previewVideo.js
├── README.md
└── workflows
├── LLM_CN.json
└── Omost_LLM.json
/.gitignore:
--------------------------------------------------------------------------------
1 | __Pycache__
--------------------------------------------------------------------------------
/flowy/lib_omost/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/requirements.txt:
--------------------------------------------------------------------------------
1 | replicate>=1.0.3
2 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test
2 |
3 | test:
4 | python -m nodes_test
--------------------------------------------------------------------------------
/images/LLM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/LLM.png
--------------------------------------------------------------------------------
/images/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/Logo.png
--------------------------------------------------------------------------------
/images/API_Key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/API_Key.png
--------------------------------------------------------------------------------
/images/clarity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/clarity.png
--------------------------------------------------------------------------------
/images/Omost_LLM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/Omost_LLM.png
--------------------------------------------------------------------------------
/images/API_Key_Node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/API_Key_Node.png
--------------------------------------------------------------------------------
/images/comflowy_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/6174/comflowy-nodes/HEAD/images/comflowy_banner.png
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/__init__.py:
--------------------------------------------------------------------------------
1 | from .replicate_bridge import REPLICATE_NODE_CLASS_MAPPINGS
2 |
3 | __all__ = ["REPLICATE_NODE_CLASS_MAPPINGS"]
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from .flowy.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
2 | WEB_DIRECTORY = "./web"
3 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
4 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "comfyui-replicate"
3 | description = "Run Replicate models in ComfyUI"
4 | version = "1.1.0"
5 | license = { file = "LICENSE" }
6 |
7 | [project.urls]
8 | Repository = "https://github.com/replicate/comfyui-replicate"
9 | # Used by Comfy Registry https://comfyregistry.org
10 |
11 | [tool.comfy]
12 | PublisherId = "fofr"
13 | DisplayName = "ComfyUI-Replicate"
14 | Icon = ""
15 |
--------------------------------------------------------------------------------
/flowy/api_nodes/__init__.py:
--------------------------------------------------------------------------------
1 | from .clarityupscaler import FlowyClarityUpscale
2 | from .flux import FlowyFlux
3 | from .fluxultra import FlowyFluxProUltra
4 | from .hailuo import FlowyHailuo
5 | from .ideogram import FlowyIdeogram
6 | from .keling import FlowyKling
7 | from .luma import FlowyLuma
8 | from .recraft import FlowyRecraft
9 | from .fluxdevlora import FlowyFluxDevLora
10 | from .replicate import REPLICATE_NODE_CLASS_MAPPINGS
11 |
12 | __all__ = [
13 | "FlowyClarityUpscale",
14 | "FlowyFlux",
15 | "FlowyFluxProUltra",
16 | "FlowyHailuo",
17 | "FlowyIdeogram",
18 | "FlowyKling",
19 | "FlowyLuma",
20 | "FlowyRecraft",
21 | "FlowyFluxDevLora",
22 | "REPLICATE_NODE_CLASS_MAPPINGS",
23 | ]
24 |
--------------------------------------------------------------------------------
/flowy/api_key_manager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import logging
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 | API_KEY_FILE = os.path.join(os.path.dirname(__file__), "api_key.json")
8 |
9 | def save_api_key(api_key):
10 | """
11 | Save the API key to a JSON file.
12 |
13 | Args:
14 | api_key (str): The API key to be saved.
15 | """
16 | try:
17 | with open(API_KEY_FILE, "w") as f:
18 | json.dump({"api_key": api_key}, f)
19 | logger.debug(f"API Key saved to {API_KEY_FILE}")
20 | except Exception as e:
21 | logger.error(f"Failed to save API Key: {str(e)}")
22 |
23 | def load_api_key():
24 | """
25 | Load the API key from the JSON file.
26 |
27 | Returns:
28 | str or None: The loaded API key, or None if not found or an error occurred.
29 | """
30 | try:
31 | if os.path.exists(API_KEY_FILE):
32 | with open(API_KEY_FILE, "r") as f:
33 | data = json.load(f)
34 | return data.get("api_key")
35 | except Exception as e:
36 | logger.error(f"Failed to load API Key: {str(e)}")
37 | return None
38 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Replicate
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/supported_models.json:
--------------------------------------------------------------------------------
1 | {
2 | "models": [
3 | "andreasjansson/blip-2",
4 | "batouresearch/high-resolution-controlnet-tile",
5 | "batouresearch/magic-image-refiner",
6 | "batouresearch/magic-style-transfer",
7 | "bytedance/sdxl-lightning-4step",
8 | "cjwbw/hyper-sdxl-1step-t2i",
9 | "cjwbw/supir",
10 | "cuuupid/glm-4v-9b",
11 | "cuuupid/idm-vton",
12 | "declare-lab/tango",
13 | "falcons-ai/nsfw_image_detection",
14 | "fofr/consistent-character",
15 | "fofr/face-to-many",
16 | "fofr/latent-consistency-model",
17 | "fofr/style-transfer",
18 | "jingyunliang/swinir",
19 | "lucataco/hunyuandit-v1.1",
20 | "lucataco/llama-3-vision-alpha",
21 | "lucataco/pasd-magnify",
22 | "lucataco/xtts-v2",
23 | "meta/meta-llama-3.1-405b-instruct",
24 | "meta/llama-2-70b-chat",
25 | "meta/llama-2-7b-chat",
26 | "meta/musicgen",
27 | "okaris/omni-zero-couples",
28 | "okaris/omni-zero",
29 | "pharmapsychotic/clip-interrogator",
30 | "salesforce/blip",
31 | "smoretalk/rembg-enhance",
32 | "stability-ai/sdxl",
33 | "stability-ai/stable-diffusion",
34 | "stability-ai/stable-diffusion-3",
35 | "suno-ai/bark",
36 | "tstramer/material-diffusion",
37 | "xiankgx/face-swap",
38 | "tencentarc/photomaker",
39 | "tencentarc/photomaker-style",
40 | "yorickvp/llava-13b",
41 | "yorickvp/llava-v1.6-34b",
42 | "yorickvp/llava-v1.6-mistral-7b",
43 | "zsxkib/realistic-voice-cloning"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/flowy/api_nodes/hailuo.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT, BOOLEAN, get_modal_cloud_web_url
3 |
4 | class FlowyHailuo(FlowyApiNode):
5 | @classmethod
6 | def INPUT_TYPES(cls):
7 | return {
8 | "required": {
9 | "image": ("IMAGE",),
10 | "prompt": ("STRING", {"multiline": True}),
11 | "prompt_optimizer": BOOLEAN,
12 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
13 | }
14 | }
15 |
16 | RETURN_TYPES = ("VIDEO",)
17 | RETURN_NAMES = ("video",)
18 | OUTPUT_IS_PREVIEW = True
19 | FUNCTION = "generate"
20 | DESCRIPTION = """
21 | Nodes from https://comflowy.com:
22 | - Description: A service to generate videos from images by Hailuo AI.
23 | - How to use:
24 | - Provide an image and a prompt.
25 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
26 | - Output: Returns the generated video.
27 | """
28 |
29 | def get_model_type(self) -> str:
30 | return "hailuo"
31 |
32 | def get_api_host(self) -> str:
33 | API_HOST = get_modal_cloud_web_url()
34 | return f"{API_HOST}/api/open/v0/flowy"
35 |
36 | def prepare_payload(self, **kwargs) -> dict:
37 | image_base64 = self.image_to_base64(kwargs["image"])
38 | return {
39 | "image": image_base64,
40 | "prompt": kwargs["prompt"],
41 | "prompt_optimizer": kwargs["prompt_optimizer"],
42 | "seed": kwargs["seed"],
43 | }
44 |
--------------------------------------------------------------------------------
/flowy/lib_omost/utils.py:
--------------------------------------------------------------------------------
1 | from contextlib import contextmanager
2 |
3 | import torch
4 | import numpy as np
5 |
6 |
7 | @torch.inference_mode()
8 | def numpy2pytorch(imgs: list[np.ndarray]):
9 | """Convert a list of numpy images to a pytorch tensor.
10 | Input: images in list[[H, W, C]] format.
11 | Output: images in [B, H, W, C] format.
12 |
13 | Note: ComfyUI expects [B, H, W, C] format instead of [B, C, H, W] format.
14 | """
15 | assert len(imgs) > 0
16 | assert all(img.ndim == 3 for img in imgs)
17 | h = torch.from_numpy(np.stack(imgs, axis=0)).float() / 255.0
18 | return h
19 |
20 |
21 | @contextmanager
22 | def scoped_numpy_random(seed: int):
23 | state = np.random.get_state() # Save the current state
24 | np.random.seed(seed) # Set the seed
25 | try:
26 | yield
27 | finally:
28 | np.random.set_state(state) # Restore the original state
29 |
30 |
31 | @contextmanager
32 | def scoped_torch_random(seed: int):
33 | cpu_state = torch.random.get_rng_state()
34 | gpu_states = []
35 | if torch.cuda.is_available():
36 | gpu_states = [
37 | torch.cuda.get_rng_state(device)
38 | for device in range(torch.cuda.device_count())
39 | ]
40 |
41 | try:
42 | torch.manual_seed(seed)
43 | if torch.cuda.is_available():
44 | torch.cuda.manual_seed_all(seed)
45 | yield
46 | finally:
47 | torch.random.set_rng_state(cpu_state)
48 | if torch.cuda.is_available():
49 | for idx, state in enumerate(gpu_states):
50 | torch.cuda.set_rng_state(state, device=idx)
51 |
--------------------------------------------------------------------------------
/flowy/nodes_previewvideo.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | # 设置日志记录器
5 | logger = logging.getLogger(__name__)
6 |
7 | class PreviewVideo:
8 | @classmethod
9 | def INPUT_TYPES(s):
10 | return {"required": {
11 | "video": ("VIDEO",),
12 | }}
13 |
14 | CATEGORY = "Comflowy"
15 | DESCRIPTION = "Preview Video Node"
16 | RETURN_TYPES = ()
17 | OUTPUT_NODE = True
18 | FUNCTION = "load_video"
19 |
20 | def load_video(self, video):
21 | logger.info(f"PreviewVideo.load_video called with video: {video}")
22 | logger.info(f"Video type: {type(video)}")
23 |
24 | # 确保视频路径是有效的字符串
25 | if not video or not isinstance(video, str):
26 | logger.error(f'Invalid video path or type: {video}')
27 | return {"ui": {"video": ["error.mp4", "output"]}}
28 |
29 | # 检查文件是否存在
30 | if not os.path.exists(video):
31 | logger.error(f'Video file does not exist at path: {video}')
32 | return {"ui": {"video": ["error.mp4", "output"]}}
33 |
34 | # 获取完整的视频文件名和目录
35 | video_filename = os.path.basename(video)
36 | video_dir = os.path.dirname(video)
37 |
38 | logger.info(f'Video filename: {video_filename}')
39 | logger.info(f'Video directory: {video_dir}')
40 | logger.info(f'Full video path: {video}')
41 | logger.info(f'File exists: {os.path.exists(video)}')
42 | logger.info(f'File size: {os.path.getsize(video)} bytes')
43 |
44 | result = {"ui": {"video": [video, video_dir]}}
45 | logger.info(f'Returning result: {result}')
46 |
47 | return result
--------------------------------------------------------------------------------
/flowy/api_nodes/fluxultra.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT, SAFETY_TOLERANCE, BOOLEAN_FALSE
3 |
4 |
5 | class FlowyFluxProUltra(FlowyApiNode):
6 | @classmethod
7 | def INPUT_TYPES(cls):
8 | return {
9 | "required": {
10 | "prompt": ("STRING", {"multiline": True}),
11 | "image_size": (
12 | ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "9:21"],
13 | ),
14 | "raw": BOOLEAN_FALSE,
15 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
16 | "safety_tolerance": (SAFETY_TOLERANCE,),
17 | "num_images": ("INT", {"default": 1, "min": 1, "max": 4}),
18 | }
19 | }
20 |
21 | RETURN_TYPES = ("IMAGE",)
22 | DESCRIPTION = """
23 | Nodes from https://comflowy.com:
24 | - Description: A service to generate images using Flux AI.
25 | - How to use:
26 | - Provide a prompt to generate an image.
27 | - Raw: Generate less processed, more natural-looking images
28 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
29 | - Output: Returns the generated image.
30 | """
31 |
32 | def get_model_type(self) -> str:
33 | return "fluxproultra"
34 |
35 | def prepare_payload(self, **kwargs) -> dict:
36 | return {
37 | "prompt": kwargs["prompt"],
38 | "image_size": kwargs["image_size"],
39 | "raw": kwargs["raw"],
40 | "seed": kwargs["seed"],
41 | "safety_tolerance": kwargs["safety_tolerance"],
42 | "num_images": kwargs["num_images"],
43 | }
44 |
--------------------------------------------------------------------------------
/flowy/api_nodes/keling.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT
3 |
4 | class FlowyKling(FlowyApiNode):
5 | @classmethod
6 | def INPUT_TYPES(cls):
7 | return {
8 | "required": {
9 | "image": ("IMAGE",),
10 | "prompt": ("STRING", {"multiline": True}),
11 | "version": (["standard", "pro"],),
12 | "aspect_ratio": (["16:9", "9:16", "1:1"],),
13 | "duration": ([5, 10],),
14 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
15 | }
16 | }
17 |
18 | RETURN_TYPES = ("VIDEO",)
19 | RETURN_NAMES = ("video",)
20 | OUTPUT_IS_PREVIEW = True
21 | FUNCTION = "generate" # Changed from image_to_video to match parent class
22 | DESCRIPTION = """
23 | Nodes from https://comflowy.com:
24 | - Description: A service to generate videos from images by Kling AI.
25 | - How to use:
26 | - Provide an image and a prompt.
27 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
28 | - Pro costs 1250 per second of video. Standard will cost 300 per second of video.
29 | - Output: Returns the generated video.
30 | """
31 |
32 | def get_model_type(self) -> str:
33 | return "kling"
34 |
35 | def prepare_payload(self, **kwargs) -> dict:
36 | image_base64 = self.image_to_base64(kwargs["image"])
37 | return {
38 | "image": image_base64,
39 | "prompt": kwargs["prompt"],
40 | "version": kwargs["version"],
41 | "aspect_ratio": kwargs["aspect_ratio"],
42 | "duration": kwargs["duration"],
43 | "seed": kwargs["seed"],
44 | }
45 |
--------------------------------------------------------------------------------
/flowy/api_nodes/luma.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT, BOOLEAN_FALSE
3 |
4 |
5 | class FlowyLuma(FlowyApiNode):
6 | @classmethod
7 | def INPUT_TYPES(cls):
8 | return {
9 | "required": {
10 | "image": ("IMAGE",),
11 | "prompt": ("STRING", {"multiline": True}),
12 | "aspect_ratio": (["16:9", "9:16", "1:1"],),
13 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
14 | },
15 | "optional": {
16 | "end_image_optional": ("IMAGE",),
17 | "loop": BOOLEAN_FALSE,
18 | },
19 | }
20 |
21 | RETURN_TYPES = ("VIDEO",)
22 | RETURN_NAMES = ("video",)
23 | OUTPUT_IS_PREVIEW = True
24 | FUNCTION = "generate" # Changed from image_to_video to match parent class
25 | DESCRIPTION = """
26 | Nodes from https://comflowy.com:
27 | - Description: A service to generate videos from images by Luma AI.
28 | - How to use:
29 | - Provide an image and a prompt.
30 | - Loop: Whether the video should loop (end of video is blended with the beginning).
31 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
32 | - Output: Returns the generated video.
33 | """
34 |
35 | def get_model_type(self) -> str:
36 | return "luma"
37 |
38 | def prepare_payload(self, **kwargs) -> dict:
39 | image_base64 = self.image_to_base64(kwargs["image"])
40 | return {
41 | "image": image_base64,
42 | "prompt": kwargs["prompt"],
43 | "aspect_ratio": kwargs["aspect_ratio"],
44 | "end_image": kwargs.get("end_image_optional"), # Optional parameter
45 | "loop": kwargs.get("loop", False), # Optional parameter with default
46 | "seed": kwargs["seed"],
47 | }
48 |
--------------------------------------------------------------------------------
/nodes_test.py:
--------------------------------------------------------------------------------
1 | import json
2 | import unittest
3 | from flowy.nodes import FlowyHttpRequest
4 |
5 | class TestFlowyHttpRequest(unittest.TestCase):
6 | def test_send_http_request_get(self):
7 | # Arrange
8 | flowy_http_request = FlowyHttpRequest()
9 | url = "https://jsonplaceholder.typicode.com/posts/1"
10 | method = "GET"
11 | headers = {}
12 | body = {}
13 | output_type = "TEXT"
14 |
15 | # Act
16 | result = flowy_http_request.send_http_request(
17 | url, method, headers, body
18 | )
19 |
20 | # Parse the result
21 | parsed_result = result["result"][0]
22 |
23 | print(parsed_result)
24 |
25 | # Assert
26 | self.assertIn("userId", parsed_result)
27 | self.assertIn("id", parsed_result)
28 | self.assertIn("title", parsed_result)
29 | self.assertIn("body", parsed_result)
30 |
31 | def test_send_http_request_post(self):
32 | # Arrange
33 | flowy_http_request = FlowyHttpRequest()
34 | url = "https://jsonplaceholder.typicode.com/posts"
35 | method = "POST"
36 | headers = {"Content-type": "application/json; charset=UTF-8"}
37 | body = {"title": "foo", "body": "bar", "userId": 1}
38 | output_type = "TEXT"
39 |
40 | # Act
41 | result = flowy_http_request.send_http_request(
42 | url, method, headers, body
43 | )
44 |
45 | # Parse the result
46 | parsed_result = result["result"][0]
47 |
48 | print(parsed_result)
49 | # Assert
50 | self.assertIn("id", parsed_result)
51 | self.assertEqual(parsed_result["title"], "foo")
52 | self.assertEqual(parsed_result["body"], "bar")
53 | self.assertEqual(parsed_result["userId"], 1)
54 |
55 | # Add more tests for PUT, DELETE, PATCH, and error handling
56 |
57 |
58 | if __name__ == "__main__":
59 | unittest.main()
60 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/import_schemas.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import replicate
3 | import json
4 | import os
5 | import argparse
6 |
7 |
8 | def format_json_file(file_path):
9 | try:
10 | with open(file_path, "r") as f:
11 | data = json.load(f)
12 | data["run_count"] = 0
13 |
14 | with open(file_path, "w") as f:
15 | json.dump(data, f, indent=4, ensure_ascii=False)
16 | except json.JSONDecodeError:
17 | print(f"Error: {file_path} contains invalid JSON")
18 | except IOError:
19 | print(f"Error: Could not read or write to {file_path}")
20 |
21 |
22 | def format_json_files_in_directory(directory):
23 | for filename in os.listdir(directory):
24 | if filename.endswith(".json"):
25 | file_path = os.path.join(directory, filename)
26 | format_json_file(file_path)
27 |
28 |
29 | def update_schemas(update=False):
30 | with open("supported_models.json", "r", encoding="utf-8") as f:
31 | supported_models = json.load(f)
32 |
33 | schemas_directory = "schemas"
34 | existing_schemas = set(os.listdir(schemas_directory))
35 |
36 | for model in supported_models["models"]:
37 | schema_filename = f"{model.replace('/', '_')}.json"
38 | schema_path = os.path.join(schemas_directory, schema_filename)
39 |
40 | if update or schema_filename not in existing_schemas:
41 | try:
42 | m = replicate.models.get(model)
43 | with open(schema_path, "w", encoding="utf-8") as f:
44 | f.write(m.json())
45 | print(f"{'Updated' if update else 'Added'} schema for {model}")
46 | except replicate.exceptions.ReplicateError as e:
47 | print(f"Error fetching schema for {model}: {str(e)}")
48 | continue
49 |
50 | format_json_files_in_directory(schemas_directory)
51 |
52 |
53 | if __name__ == "__main__":
54 | parser = argparse.ArgumentParser(description="Update model schemas")
55 | parser.add_argument("--update", action="store_true", help="Update all schemas, not just new ones")
56 | args = parser.parse_args()
57 |
58 | update_schemas(update=args.update)
59 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/README.md:
--------------------------------------------------------------------------------
1 | # comfyui-replicate
2 |
3 | Custom nodes for running [Replicate models](https://replicate.com/explore) in ComfyUI.
4 |
5 | Take a look at the [example workflows](https://github.com/replicate/comfyui-replicate/tree/main/example_workflows) and the [supported Replicate models](https://github.com/replicate/comfyui-replicate/blob/main/supported_models.json) to get started.
6 |
7 | 
8 |
9 | ## Set your Replicate API token before running
10 |
11 | Make sure you set your REPLICATE_API_TOKEN in your environment. Get your API tokens here, we recommend creating a new one:
12 |
13 | https://replicate.com/account/api-tokens
14 |
15 | To pass in your API token when running ComfyUI you could do:
16 |
17 | On MacOS or Linux:
18 |
19 | ```sh
20 | export REPLICATE_API_TOKEN="r8_************"; python main.py
21 | ```
22 |
23 | On Windows:
24 |
25 | ```sh
26 | set REPLICATE_API_TOKEN="r8_************"; python main.py
27 | ```
28 |
29 | ## Direct installation
30 |
31 | ```sh
32 | cd ComfyUI/custom-nodes
33 | git clone https://github.com/replicate/comfyui-replicate
34 | cd comfyui-replicate
35 | pip install -r requirements.txt
36 | ```
37 |
38 | ## Supported Replicate models
39 |
40 | View the `supported_models.json` to see which models are packaged by default.
41 |
42 | ## Update Replicate models
43 |
44 | Simply run `./import_schemas.py` to update all model nodes. The latest version of a model is used by default.
45 |
46 | ## Add more models
47 |
48 | Only models that return simple text or image outputs are currently supported. If a model returns audio, video, JSON objects or a combination of outputs, the node will not work as expected.
49 |
50 | If you want to add more models, you can:
51 |
52 | - add the model to `supported_models.json` (for example, `fofr/consistent-character`)
53 | - run `./import_schemas.py`, this will update all schemas and import your new model
54 | - restart ComfyUI
55 | - use the model in workflow, it’ll have the title ‘Replicate [model author/model name]’
56 |
57 | ## Roadmap
58 |
59 | Things to investigate and add to this custom node package:
60 |
61 | - support for more types of Replicate model (audio and video first)
62 | - showing logs, prediction status and progress (via tqdm)
63 |
64 | ## Contributing
65 |
66 | If you add models that others would find useful, feel free to raise PRs.
67 |
--------------------------------------------------------------------------------
/flowy/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import requests
3 | from .types import API_HOST
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 | def llm_request(prompt, system_prompt, llm_model, api_key, max_tokens=3000, timeout=10):
8 | """
9 | Send a request to the Comflowy LLM API.
10 |
11 | Args:
12 | prompt (str): The main prompt for the LLM.
13 | system_prompt (str): The system prompt for the LLM.
14 | llm_model (str): The LLM model to use.
15 | api_key (str): The API key for authentication.
16 | max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 3000.
17 | timeout (int, optional): Timeout for the request in seconds. Defaults to 10.
18 |
19 | Returns:
20 | str: The generated text from the LLM.
21 |
22 | Raises:
23 | Exception: If there's an error in the API request or response.
24 | """
25 | try:
26 | response = requests.post(
27 | f"{API_HOST}/api/open/v0/prompt",
28 | headers={
29 | "Content-Type": "application/json",
30 | "Authorization": f"Bearer {api_key}",
31 | },
32 | json={
33 | "prompt": prompt,
34 | "system_prompt": system_prompt,
35 | "model": llm_model,
36 | "max_tokens": max_tokens,
37 | },
38 | timeout=timeout,
39 | )
40 |
41 | response.raise_for_status() # Raise an HTTPError for bad responses
42 |
43 | ret = response.json()
44 | if ret.get("success"):
45 | return ret.get("text")
46 | else:
47 | raise Exception(f"Error: {ret.get('error')}")
48 | except Exception as e:
49 | raise Exception(f"Failed to get response from LLM model with {API_HOST}/api/open/v0/prompt, error: {str(e)}")
50 |
51 | def get_nested_value(obj, path, default=None):
52 | """
53 | Get a nested value from a dictionary using a dot-separated path.
54 |
55 | Args:
56 | obj (dict): The dictionary to search in.
57 | path (str): The dot-separated path to the desired value.
58 | default: The value to return if the path is not found.
59 |
60 | Returns:
61 | The value at the specified path, or the default value if not found.
62 | """
63 | keys = path.split('.')
64 | for key in keys:
65 | if isinstance(obj, dict):
66 | obj = obj.get(key, default)
67 | else:
68 | return default
69 | return obj
70 |
71 | # Make sure to export the functions
72 | __all__ = ['llm_request', 'get_nested_value']
73 |
--------------------------------------------------------------------------------
/flowy/nodes_llm.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from .api_key_manager import load_api_key
3 | from .types import (
4 | API_HOST,
5 | LLM_MODELS,
6 | STRING,
7 | STRING_ML,
8 | )
9 | from .utils import llm_request
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 | class FlowyLLM:
14 | """
15 | A node for making requests to the Comflowy LLM service.
16 | """
17 | @classmethod
18 | def INPUT_TYPES(s):
19 | return {
20 | "required": {
21 | "prompt": STRING_ML,
22 | "system_prompt": STRING_ML,
23 | "llm_model": (LLM_MODELS,),
24 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
25 | }
26 | }
27 |
28 | RETURN_TYPES = ("STRING",)
29 | FUNCTION = "llm_request"
30 | OUTPUT_NODE = True
31 | CATEGORY = "Comflowy"
32 | DESCRIPTION = """
33 | Nodes from https://comflowy.com:
34 | - Description: A free service to send a prompt to a LLM model, and get the response.
35 | - How to use:
36 | - Provide a prompt and a system prompt to generate a response from the LLM model.
37 | - Choose the LLM model from the available options.
38 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
39 | - Output: Return the generated text from the LLM model.
40 | """
41 |
42 | def llm_request(self, prompt, system_prompt, llm_model, seed, timeout=10):
43 | """
44 | Make a request to the Comflowy LLM service.
45 |
46 | Args:
47 | prompt (str): The main prompt for the LLM.
48 | system_prompt (str): The system prompt for the LLM.
49 | llm_model (str): The LLM model to use.
50 | seed (int): The seed for random number generation.
51 | timeout (int, optional): Timeout for the request in seconds. Defaults to 10.
52 |
53 | Returns:
54 | dict: A dictionary containing the UI output and the result.
55 | """
56 | if seed > 0xFFFFFFFF:
57 | seed = seed & 0xFFFFFFFF
58 | logger.warning("Seed is too large. Truncating to 32-bit: %d", seed)
59 |
60 | api_key = load_api_key()
61 |
62 | if not api_key:
63 | error_msg = "API Key is not set. Please use the 'Comflowy Set API Key' node to set a global API Key before using this node."
64 | logger.error(error_msg)
65 | raise ValueError(error_msg)
66 |
67 | try:
68 | generated_text = llm_request(
69 | prompt=prompt,
70 | llm_model=llm_model,
71 | system_prompt=system_prompt,
72 | api_key=api_key,
73 | max_tokens=4000,
74 | timeout=10
75 | )
76 | return {"ui": {"text": [generated_text]}, "result": (generated_text,)}
77 | except Exception as e:
78 | logger.error(f"Error in LLM request: {str(e)}")
79 | return {"ui": {"text": [str(e)]}, "result": (str(e),)}
--------------------------------------------------------------------------------
/flowy/api_nodes/flux.py:
--------------------------------------------------------------------------------
1 | import time
2 | import requests
3 | import base64
4 | import io
5 | from PIL import Image
6 | import torch
7 | import numpy as np
8 | import logging
9 | import json
10 | from ..types import STRING, INT, SAFETY_TOLERANCE, BOOLEAN, NUM_IMAGES
11 | from ..utils import logger, get_nested_value
12 | from ..api_key_manager import load_api_key
13 |
14 | logger = logging.getLogger(__name__)
15 | from .base import FlowyApiNode
16 |
17 | class FlowyFlux(FlowyApiNode):
18 | @classmethod
19 | def INPUT_TYPES(cls):
20 | return {
21 | "required": {
22 | "prompt": ("STRING", {"multiline": True}),
23 | "version": (["flux-1.1-pro", "flux-pro", "flux-dev"],),
24 | "image_size": (
25 | [
26 | "square_hd",
27 | "square",
28 | "portrait_4_3",
29 | "portrait_16_9",
30 | "landscape_4_3",
31 | "landscape_16_9",
32 | "custom",
33 | ],
34 | ),
35 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
36 | "safety_tolerance": (SAFETY_TOLERANCE,),
37 | "num_images": (NUM_IMAGES,),
38 | },
39 | "optional": {
40 | "height": (
41 | "INT",
42 | {
43 | "default": 512,
44 | "min": 256,
45 | "max": 2048,
46 | "hidden": "image_size != 'custom'",
47 | },
48 | ),
49 | "width": (
50 | "INT",
51 | {
52 | "default": 512,
53 | "min": 256,
54 | "max": 2048,
55 | "hidden": "image_size != 'custom'",
56 | },
57 | ),
58 | },
59 | }
60 |
61 | RETURN_TYPES = ("IMAGE",)
62 | FUNCTION = "generate"
63 | DESCRIPTION = """Nodes from https://comflowy.com:
64 | - Description: A service to generate images using Flux AI.
65 | - How to use:
66 | - Provide a prompt to generate an image.
67 | - Choose version, image_size, height, width, and seed.
68 | - Height and width are only used when image_size=custom.
69 | - Make sure to set your API Key using the 'Comflowy Set API Key' node first.
70 | - Output: Returns the generated image."""
71 |
72 | def get_model_type(self) -> str:
73 | return "flux"
74 |
75 | def prepare_payload(self, **kwargs) -> dict:
76 | return {
77 | "prompt": kwargs["prompt"],
78 | "version": kwargs["version"],
79 | "image_size": kwargs["image_size"],
80 | "height": kwargs["height"],
81 | "width": kwargs["width"],
82 | "seed": kwargs["seed"],
83 | "safety_tolerance": kwargs["safety_tolerance"],
84 | "num_images": kwargs["num_images"],
85 | }
86 |
--------------------------------------------------------------------------------
/flowy/nodes_http.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 |
4 | # You can use this node to save full size images through the websocket, the
5 | # images will be sent in exactly the same format as the image previews: as
6 | # binary images on the websocket with a 8 byte header indicating the type
7 | # of binary message (first 4 bytes) and the image format (next 4 bytes).
8 |
9 | # Note that no metadata will be put in the images saved with this node.
10 | from .types import (
11 | HTTP_REQUEST_METHOD,
12 | STRING,
13 | STRING_ML,
14 | )
15 |
16 | class FlowyHttpRequest:
17 | @classmethod
18 | def INPUT_TYPES(s):
19 | return {
20 | "required": {"url": STRING, "method": (HTTP_REQUEST_METHOD,)},
21 | "optional": {
22 | "headers_json": STRING_ML,
23 | "body_json": STRING_ML,
24 | # "json_path": STRING,
25 | },
26 | }
27 |
28 | RETURN_TYPES = ("JSON",)
29 | FUNCTION = "send_http_request"
30 | OUTPUT_NODE = True
31 | CATEGORY = "Comflowy"
32 | DESCRIPTION = """
33 | Nodes from https://comflowy.com:
34 | - Description: Send an HTTP request to a URL.
35 | - Output: Return a JSON result from the request.
36 | """
37 |
38 | def send_http_request(self, url, method, headers_json, body_json):
39 | try:
40 | timeout = 10
41 | try:
42 | headers = json.loads(headers_json) if headers_json else {}
43 | body = json.loads(body_json) if body_json else {}
44 | except Exception as e:
45 | raise ValueError(f"Invalid headers or body: {e}")
46 | print("http request with", url, headers, body)
47 |
48 | response = None
49 | if method == "GET":
50 | response = requests.get(url, headers=headers, timeout=timeout)
51 | elif method == "POST":
52 | response = requests.post(
53 | url, headers=headers, json=body, timeout=timeout
54 | )
55 | elif method == "PUT":
56 | response = requests.put(
57 | url, headers=headers, json=body, timeout=timeout
58 | )
59 | elif method == "DELETE":
60 | response = requests.delete(url, headers=headers, timeout=timeout)
61 | elif method == "PATCH":
62 | response = requests.patch(
63 | url, headers=headers, json=body, timeout=timeout
64 | )
65 | else:
66 | raise ValueError(f"Invalid method {method}")
67 |
68 | response.raise_for_status() # Raise an HTTPError for bad responses
69 |
70 | ret = response.json()
71 |
72 | print("http request result", ret)
73 | # if json_path:
74 | # print("json_path", json_path)
75 | # ret = get_nested_value[json_path]
76 |
77 | return {"ui": {"text": [json.dumps(ret, indent=4)]}, "result": (ret,)}
78 |
79 | except requests.exceptions.RequestException as e:
80 | print("http request error", e)
81 | return {"ui": {"text": [str(e)]}, "result": (str(e),)}
82 |
--------------------------------------------------------------------------------
/flowy/api_nodes/ideogram.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT, get_api_host
3 |
4 |
5 | class FlowyIdeogram(FlowyApiNode):
6 | @classmethod
7 | def INPUT_TYPES(cls):
8 | return {
9 | "required": {
10 | "prompt": ("STRING", {"multiline": True}),
11 | "negative_prompt": ("STRING", {"multiline": True}),
12 | "version": (["ideogram-v2-turbo", "ideogram-v2"],),
13 | "resolution": ( [ "None", "512x1536", "576x1408", "576x1472", "576x1536", "640x1024", "640x1344", "640x1408", "640x1472", "640x1536", "704x1152", "704x1216", "704x1280", "704x1344", "704x1408", "704x1472", "720x1280", "736x1312", "768x1024", "768x1088", "768x1152", "768x1216", "768x1232", "768x1280", "768x1344", "832x960", "832x1024", "832x1088", "832x1152", "832x1216", "832x1248", "864x1152", "896x960", "896x1024", "896x1088", "896x1120", "896x1152", "960x832", "960x896", "960x1024", "960x1088", "1024x640", "1024x768", "1024x832", "1024x896", "1024x960", "1024x1024", "1088x768", "1088x832", "1088x896", "1088x960", "1120x896", "1152x704", "1152x768", "1152x832", "1152x864", "1152x896", "1216x704", "1216x768", "1216x832", "1232x768", "1248x832", "1280x704", "1280x720", "1280x768", "1280x800", "1312x736", "1344x640", "1344x704", "1344x768", "1408x576", "1408x640", "1408x704", "1472x576", "1472x640", "1472x704", "1536x512", "1536x576", "1536x640", ], ),
14 | "style_type": (
15 | ["None", "Auto", "Realistic", "Design", "Anime", "Render 3D"],
16 | ),
17 | "aspect_ratio": ( [ "1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "16:10", "10:16", "3:1", "1:3", ], ),
18 | "magic_prompt_option": (["On", "Off"],),
19 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
20 | }
21 | }
22 |
23 | RETURN_TYPES = ("IMAGE",)
24 | DESCRIPTION = """
25 | Nodes from https://comflowy.com:
26 | - Description: A service to generate images using Ideogram AI.
27 | - How to use:
28 | - Provide a prompt to generate an image.
29 | - Choose resolution, style type, aspect ratio, and magic prompt option.
30 | - Resolution overrides aspect ratio.
31 | - Magic Prompt will interpret your prompt and optimize it to maximize variety and quality of the images generated. You can also use it to write prompts in different languages.
32 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
33 | - Output: Returns the generated image.
34 | """
35 |
36 | def get_model_type(self) -> str:
37 | return "ideogram"
38 |
39 | def get_api_host(self) -> str:
40 | API_HOST = get_api_host()
41 | return f"{API_HOST}/api/open/v0/ideogram"
42 |
43 | def prepare_payload(self, **kwargs) -> dict:
44 | return {
45 | "prompt": kwargs["prompt"],
46 | "negative_prompt": kwargs["negative_prompt"],
47 | "version": kwargs["version"],
48 | "resolution": (
49 | kwargs["resolution"] if kwargs["resolution"] != "None" else None
50 | ),
51 | "style_type": (
52 | kwargs["style_type"] if kwargs["style_type"] != "None" else None
53 | ),
54 | "aspect_ratio": kwargs["aspect_ratio"],
55 | "magic_prompt_option": kwargs["magic_prompt_option"],
56 | "seed": kwargs["seed"],
57 | }
58 |
--------------------------------------------------------------------------------
/flowy/types.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | import os
4 |
5 | API_HOST = "https://app.comflowy.com"
6 | # API_HOST = "http://127.0.0.1:3000"
7 | PPT_TOKEN = ""
8 | RUN_ID = ""
9 | ENV = "pro"
10 | # ENV = "preview"
11 |
12 | def _read_config():
13 | try:
14 | # 首先尝试从线程上下文获取
15 | thread_context_module = sys.modules.get("flowy_execute_thread_context")
16 | if thread_context_module and hasattr(thread_context_module, "get_run_context"):
17 | options = thread_context_module.get_run_context("options")
18 | if options:
19 | print("get_run_context", options.get("custom_node_api_config", {}))
20 | return options.get("custom_node_api_config", {})
21 | except Exception as e:
22 | print(f"Error getting context from thread: {e}")
23 |
24 | # 回退到文件读取
25 | try:
26 | config_path = "/comfyui/custom_node_api_config.json"
27 | if os.path.exists(config_path):
28 | with open(config_path, "r") as f:
29 | ret = json.load(f)
30 | print("read_config", ret)
31 | return ret
32 | except Exception as e:
33 | print(f"Error reading custom node api config file: {e}")
34 |
35 | # 如果都失败了,返回空字典
36 | return {}
37 |
38 |
39 | def get_api_host():
40 | config = _read_config()
41 | return config.get("domain", API_HOST)
42 |
43 | def get_modal_cloud_web_url():
44 | config = _read_config()
45 | env = config.get("env", ENV)
46 | if env == "dev":
47 | return "https://comflowy--cloud-web-dev.modal.run"
48 | else:
49 | return "https://comflowy--comflowyspacecloud-web-main.modal.run"
50 |
51 | def get_ppt_token():
52 | config = _read_config()
53 | return config.get("ppt_token", PPT_TOKEN)
54 |
55 | def get_run_id():
56 | config = _read_config()
57 | return config.get("run_id", RUN_ID)
58 |
59 | FLOAT = (
60 | "FLOAT",
61 | {"default": 1, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 0.01},
62 | )
63 |
64 | BOOLEAN = ("BOOLEAN", {"default": True})
65 | BOOLEAN_FALSE = ("BOOLEAN", {"default": False})
66 |
67 | INT = ("INT", {"default": 1, "min": -sys.maxsize, "max": sys.maxsize, "step": 1})
68 |
69 | STRING = ("STRING", {"default": ""})
70 |
71 | STRING_ML = ("STRING", {"multiline": True, "default": ""})
72 |
73 | STRING_WIDGET = ("STRING", {"forceInput": True})
74 |
75 | JSON_WIDGET = ("JSON", {"forceInput": True})
76 |
77 | METADATA_RAW = ("METADATA_RAW", {"forceInput": True})
78 |
79 | HTTP_REQUEST_METHOD = ["GET", "POST", "PUT", "DELETE", "PATCH"]
80 |
81 | HTTP_REQUEST_TYPE = ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"]
82 |
83 | HTTP_REQUEST_RETURN_TYPE = ["TEXT", "JSON"]
84 |
85 | LLM_MODELS = [
86 | "Qwen/Qwen2-7B-Instruct",
87 | "Qwen/Qwen2-1.5B-Instruct",
88 | "THUDM/glm-4-9b-chat",
89 | "THUDM/chatglm3-6b",
90 | "01-ai/Yi-1.5-9B-Chat-16K",
91 | "01-ai/Yi-1.5-6B-Chat",
92 | "internlm/internlm2_5-7b-chat"
93 | ]
94 |
95 | SAFETY_TOLERANCE = ["1", "2", "3", "4", "5", "6"]
96 |
97 | NUM_IMAGES = ["1", "2", "3", "4"]
98 |
99 | OUTPUT_FORMAT = ["jpeg", "png"]
100 |
101 | class AnyType(str):
102 | """A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
103 |
104 | def __eq__(self, _) -> bool:
105 | return True
106 |
107 | def __ne__(self, __value: object) -> bool:
108 | return False
109 |
110 | any = AnyType("*")
111 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/example_workflows/bark_and_musicgen.json:
--------------------------------------------------------------------------------
1 | {
2 | "last_node_id": 7,
3 | "last_link_id": 2,
4 | "nodes": [
5 | {
6 | "id": 5,
7 | "type": "Replicate suno-ai/bark",
8 | "pos": [
9 | 540,
10 | 264
11 | ],
12 | "size": {
13 | "0": 400,
14 | "1": 266
15 | },
16 | "flags": {},
17 | "order": 0,
18 | "mode": 0,
19 | "outputs": [
20 | {
21 | "name": "AUDIO",
22 | "type": "AUDIO",
23 | "links": [
24 | 1
25 | ],
26 | "shape": 3,
27 | "slot_index": 0
28 | },
29 | {
30 | "name": "STRING",
31 | "type": "STRING",
32 | "links": null,
33 | "shape": 3
34 | }
35 | ],
36 | "properties": {
37 | "Node name for S&R": "Replicate suno-ai/bark"
38 | },
39 | "widgets_values": [
40 | "Hello, my name is Suno. And, uh — and I like pizza. [laughs] But I also have other interests such as playing tic tac toe.",
41 | "announcer",
42 | "",
43 | 0.7,
44 | 0.7,
45 | false,
46 | false
47 | ]
48 | },
49 | {
50 | "id": 7,
51 | "type": "PreviewAudio",
52 | "pos": [
53 | 1457,
54 | 272
55 | ],
56 | "size": {
57 | "0": 315,
58 | "1": 76
59 | },
60 | "flags": {},
61 | "order": 2,
62 | "mode": 0,
63 | "inputs": [
64 | {
65 | "name": "audio",
66 | "type": "AUDIO",
67 | "link": 2
68 | }
69 | ],
70 | "properties": {
71 | "Node name for S&R": "PreviewAudio"
72 | },
73 | "widgets_values": [
74 | null
75 | ]
76 | },
77 | {
78 | "id": 6,
79 | "type": "Replicate meta/musicgen",
80 | "pos": [
81 | 1001,
82 | 269
83 | ],
84 | "size": {
85 | "0": 400,
86 | "1": 436
87 | },
88 | "flags": {},
89 | "order": 1,
90 | "mode": 0,
91 | "inputs": [
92 | {
93 | "name": "input_audio",
94 | "type": "AUDIO",
95 | "link": 1
96 | }
97 | ],
98 | "outputs": [
99 | {
100 | "name": "AUDIO",
101 | "type": "AUDIO",
102 | "links": [
103 | 2
104 | ],
105 | "shape": 3,
106 | "slot_index": 0
107 | }
108 | ],
109 | "properties": {
110 | "Node name for S&R": "Replicate meta/musicgen"
111 | },
112 | "widgets_values": [
113 | "stereo-melody-large",
114 | "rap, rock",
115 | 20,
116 | true,
117 | 0,
118 | 6,
119 | false,
120 | "loudness",
121 | 250,
122 | 0,
123 | 1,
124 | 3,
125 | "wav",
126 | 1607,
127 | "randomize",
128 | false
129 | ]
130 | }
131 | ],
132 | "links": [
133 | [
134 | 1,
135 | 5,
136 | 0,
137 | 6,
138 | 0,
139 | "AUDIO"
140 | ],
141 | [
142 | 2,
143 | 6,
144 | 0,
145 | 7,
146 | 0,
147 | "AUDIO"
148 | ]
149 | ],
150 | "groups": [],
151 | "config": {},
152 | "extra": {
153 | "ds": {
154 | "scale": 1,
155 | "offset": [
156 | -123.2666015625,
157 | -48.2666015625
158 | ]
159 | }
160 | },
161 | "version": 0.4
162 | }
--------------------------------------------------------------------------------
/flowy/api_nodes/fluxdevlora.py:
--------------------------------------------------------------------------------
1 | import time
2 | import requests
3 | import base64
4 | import io
5 | from PIL import Image
6 | import torch
7 | import numpy as np
8 | import logging
9 | import json
10 | from ..types import STRING, INT, SAFETY_TOLERANCE, BOOLEAN, FLOAT, NUM_IMAGES, OUTPUT_FORMAT
11 | from ..utils import logger, get_nested_value
12 | from ..api_key_manager import load_api_key
13 |
14 | logger = logging.getLogger(__name__)
15 | from .base import FlowyApiNode
16 |
17 | class FlowyFluxDevLora(FlowyApiNode):
18 | @classmethod
19 | def INPUT_TYPES(cls):
20 | return {
21 | "required": {
22 | "prompt": ("STRING", {"multiline": True}),
23 | "lora_path": ("STRING", {"multiline": True}),
24 | "lora_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 4.0, "step": 0.1}),
25 | "image_size": (
26 | [
27 | "square_hd",
28 | "square",
29 | "portrait_4_3",
30 | "portrait_16_9",
31 | "landscape_4_3",
32 | "landscape_16_9",
33 | "custom",
34 | ],
35 | ),
36 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
37 | "num_inference_steps": ("INT", {"default": 28, "min": 1, "max": 50}),
38 | "guidance_scale": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 20.0, "step": 0.1}),
39 | "num_images": (NUM_IMAGES,),
40 | "safety_tolerance": (SAFETY_TOLERANCE,),
41 | "output_format": (OUTPUT_FORMAT,),
42 | },
43 | "optional": {
44 | "height": (
45 | "INT",
46 | {
47 | "default": 512,
48 | "min": 256,
49 | "max": 2048,
50 | "hidden": "image_size != 'custom'",
51 | },
52 | ),
53 | "width": (
54 | "INT",
55 | {
56 | "default": 512,
57 | "min": 256,
58 | "max": 2048,
59 | "hidden": "image_size != 'custom'",
60 | },
61 | ),
62 | },
63 | }
64 |
65 | RETURN_TYPES = ("IMAGE",)
66 | FUNCTION = "generate"
67 | DESCRIPTION = """Nodes from https://comflowy.com:
68 | - Description: A service to generate images using Flux AI.
69 | - How to use:
70 | - Provide a prompt to generate an image.
71 | - Choose version, image_size, height, width, seed, and other parameters.
72 | - Height and width are only used when image_size=custom.
73 | - Make sure to set your API Key using the 'Comflowy Set API Key' node first.
74 | - Output: Returns the generated image."""
75 |
76 | def get_model_type(self) -> str:
77 | return "fluxdevlora"
78 |
79 | def prepare_payload(self, **kwargs) -> dict:
80 | return {
81 | "prompt": kwargs["prompt"],
82 | "lora_path": kwargs["lora_path"],
83 | "lora_scale": kwargs["lora_scale"],
84 | "image_size": kwargs["image_size"],
85 | "seed": kwargs["seed"],
86 | "num_inference_steps": kwargs["num_inference_steps"],
87 | "guidance_scale": kwargs["guidance_scale"],
88 | "num_images": kwargs["num_images"],
89 | "safety_tolerance": kwargs["safety_tolerance"],
90 | "output_format": kwargs["output_format"],
91 | "height": kwargs["height"],
92 | "width": kwargs["width"],
93 | }
94 |
--------------------------------------------------------------------------------
/flowy/api_nodes/recraft.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT
3 |
4 | class FlowyRecraft(FlowyApiNode):
5 | @classmethod
6 | def INPUT_TYPES(cls):
7 | return {
8 | "required": {
9 | "prompt": ("STRING", {"multiline": True}),
10 | "image_size": (
11 | [
12 | "square_hd",
13 | "square",
14 | "portrait_4_3",
15 | "portrait_16_9",
16 | "landscape_4_3",
17 | "landscape_16_9",
18 | "custom",
19 | ],
20 | ),
21 | "style": (
22 | [
23 | "realistic_image",
24 | "digital_illustration",
25 | "vector_illustration",
26 | "realistic_image/b_and_w",
27 | "realistic_image/hard_flash",
28 | "realistic_image/hdr",
29 | "realistic_image/natural_light",
30 | "realistic_image/studio_portrait",
31 | "realistic_image/enterprise",
32 | "realistic_image/motion_blur",
33 | "digital_illustration/pixel_art",
34 | "digital_illustration/hand_drawn",
35 | "digital_illustration/grain",
36 | "digital_illustration/infantile_sketch",
37 | "digital_illustration/2d_art_poster",
38 | "digital_illustration/handmade_3d",
39 | "digital_illustration/hand_drawn_outline",
40 | "digital_illustration/engraving_color",
41 | "digital_illustration/2d_art_poster_2",
42 | "vector_illustration/engraving",
43 | "vector_illustration/line_art",
44 | "vector_illustration/line_circuit",
45 | "vector_illustration/linocut",
46 | ],
47 | ),
48 | "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
49 | },
50 | "optional": {
51 | "height": (
52 | "INT",
53 | {
54 | "default": 512,
55 | "min": 256,
56 | "max": 2048,
57 | "hidden": "image_size != 'custom'",
58 | },
59 | ),
60 | "width": (
61 | "INT",
62 | {
63 | "default": 512,
64 | "min": 256,
65 | "max": 2048,
66 | "hidden": "image_size != 'custom'",
67 | },
68 | ),
69 | },
70 | }
71 |
72 | RETURN_TYPES = ("IMAGE",)
73 | DESCRIPTION = """
74 | Nodes from https://comflowy.com:
75 | - Description: A service to generate images using Recraft AI.
76 | - How to use:
77 | - Provide a prompt to generate an image.
78 | - Style: The style of the generated images. Vector images cost 2X as much.
79 | - Output: Returns the generated image.
80 | """
81 |
82 | def get_model_type(self) -> str:
83 | return "recraft"
84 |
85 | def prepare_payload(self, **kwargs) -> dict:
86 | return {
87 | "prompt": kwargs["prompt"],
88 | "image_size": kwargs["image_size"],
89 | "height": kwargs.get("height"),
90 | "width": kwargs.get("width"),
91 | "style": kwargs["style"],
92 | "seed": kwargs["seed"],
93 | }
94 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/example_workflows/simple-llava.json:
--------------------------------------------------------------------------------
1 | {
2 | "last_node_id": 5,
3 | "last_link_id": 4,
4 | "nodes": [
5 | {
6 | "id": 4,
7 | "type": "Replicate yorickvp/llava-v1.6-34b",
8 | "pos": [
9 | 900,
10 | 386
11 | ],
12 | "size": {
13 | "0": 400,
14 | "1": 200
15 | },
16 | "flags": {},
17 | "order": 1,
18 | "mode": 0,
19 | "inputs": [
20 | {
21 | "name": "image",
22 | "type": "IMAGE",
23 | "link": 3
24 | }
25 | ],
26 | "outputs": [
27 | {
28 | "name": "STRING",
29 | "type": "STRING",
30 | "links": [
31 | 4
32 | ],
33 | "shape": 3,
34 | "slot_index": 0
35 | }
36 | ],
37 | "properties": {
38 | "Node name for S&R": "Replicate yorickvp/llava-v1.6-34b"
39 | },
40 | "widgets_values": [
41 | "Describe this image",
42 | 1,
43 | 0.2,
44 | 1024,
45 | "",
46 | false
47 | ]
48 | },
49 | {
50 | "id": 5,
51 | "type": "ShowText|pysssss",
52 | "pos": [
53 | 1348,
54 | 390
55 | ],
56 | "size": [
57 | 366.6171875,
58 | 247.916015625
59 | ],
60 | "flags": {},
61 | "order": 2,
62 | "mode": 0,
63 | "inputs": [
64 | {
65 | "name": "text",
66 | "type": "STRING",
67 | "link": 4,
68 | "widget": {
69 | "name": "text"
70 | }
71 | }
72 | ],
73 | "outputs": [
74 | {
75 | "name": "STRING",
76 | "type": "STRING",
77 | "links": null,
78 | "shape": 6
79 | }
80 | ],
81 | "properties": {
82 | "Node name for S&R": "ShowText|pysssss"
83 | },
84 | "widgets_values": [
85 | "",
86 | "The image appears to be a stylized drawing of two individuals, likely a man and a woman, depicted in a cartoon or caricature style. The man has short, light-colored hair and a beard, and is wearing a dark suit with a white shirt and a black tie. The woman has long, blonde hair and is wearing what seems to be a black dress. Both characters are smiling and looking directly at the viewer. The background is a solid light blue color. There is a watermark or logo in the upper right corner of the image, but the text is not legible in this description. The overall style of the image is playful and artistic, with a focus on the facial features and expressions of the characters."
87 | ]
88 | },
89 | {
90 | "id": 1,
91 | "type": "LoadImage",
92 | "pos": [
93 | 551,
94 | 393
95 | ],
96 | "size": [
97 | 315,
98 | 314
99 | ],
100 | "flags": {},
101 | "order": 0,
102 | "mode": 0,
103 | "outputs": [
104 | {
105 | "name": "IMAGE",
106 | "type": "IMAGE",
107 | "links": [
108 | 3
109 | ],
110 | "shape": 3,
111 | "slot_index": 0
112 | },
113 | {
114 | "name": "MASK",
115 | "type": "MASK",
116 | "links": null,
117 | "shape": 3
118 | }
119 | ],
120 | "properties": {
121 | "Node name for S&R": "LoadImage"
122 | },
123 | "widgets_values": [
124 | "R8__00002_-4.webp",
125 | "image"
126 | ]
127 | }
128 | ],
129 | "links": [
130 | [
131 | 3,
132 | 1,
133 | 0,
134 | 4,
135 | 0,
136 | "IMAGE"
137 | ],
138 | [
139 | 4,
140 | 4,
141 | 0,
142 | 5,
143 | 0,
144 | "STRING"
145 | ]
146 | ],
147 | "groups": [],
148 | "config": {},
149 | "extra": {
150 | "ds": {
151 | "scale": 1,
152 | "offset": [
153 | 0,
154 | 0
155 | ]
156 | }
157 | },
158 | "version": 0.4
159 | }
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/example_workflows/simple-garment-try-on.json:
--------------------------------------------------------------------------------
1 | {
2 | "last_node_id": 4,
3 | "last_link_id": 3,
4 | "nodes": [
5 | {
6 | "id": 3,
7 | "type": "LoadImage",
8 | "pos": [
9 | 237,
10 | 507
11 | ],
12 | "size": [
13 | 315,
14 | 314
15 | ],
16 | "flags": {},
17 | "order": 0,
18 | "mode": 0,
19 | "outputs": [
20 | {
21 | "name": "IMAGE",
22 | "type": "IMAGE",
23 | "links": [
24 | 1
25 | ],
26 | "shape": 3,
27 | "slot_index": 0
28 | },
29 | {
30 | "name": "MASK",
31 | "type": "MASK",
32 | "links": null,
33 | "shape": 3
34 | }
35 | ],
36 | "properties": {
37 | "Node name for S&R": "LoadImage"
38 | },
39 | "widgets_values": [
40 | "KakaoTalk_Photo_2024-04-04-21-44-45-1.png",
41 | "image"
42 | ]
43 | },
44 | {
45 | "id": 2,
46 | "type": "LoadImage",
47 | "pos": [
48 | 240,
49 | 137
50 | ],
51 | "size": [
52 | 315,
53 | 314
54 | ],
55 | "flags": {},
56 | "order": 1,
57 | "mode": 0,
58 | "outputs": [
59 | {
60 | "name": "IMAGE",
61 | "type": "IMAGE",
62 | "links": [
63 | 2
64 | ],
65 | "shape": 3,
66 | "slot_index": 0
67 | },
68 | {
69 | "name": "MASK",
70 | "type": "MASK",
71 | "links": null,
72 | "shape": 3
73 | }
74 | ],
75 | "properties": {
76 | "Node name for S&R": "LoadImage"
77 | },
78 | "widgets_values": [
79 | "sweater.webp",
80 | "image"
81 | ]
82 | },
83 | {
84 | "id": 1,
85 | "type": "Replicate cuuupid/idm-vton",
86 | "pos": [
87 | 915,
88 | 300
89 | ],
90 | "size": {
91 | "0": 315,
92 | "1": 290
93 | },
94 | "flags": {},
95 | "order": 2,
96 | "mode": 0,
97 | "inputs": [
98 | {
99 | "name": "garm_img",
100 | "type": "IMAGE",
101 | "link": 2
102 | },
103 | {
104 | "name": "human_img",
105 | "type": "IMAGE",
106 | "link": 1
107 | },
108 | {
109 | "name": "mask_img",
110 | "type": "IMAGE",
111 | "link": null
112 | }
113 | ],
114 | "outputs": [
115 | {
116 | "name": "IMAGE",
117 | "type": "IMAGE",
118 | "links": [
119 | 3
120 | ],
121 | "shape": 3,
122 | "slot_index": 0
123 | }
124 | ],
125 | "properties": {
126 | "Node name for S&R": "Replicate cuuupid/idm-vton"
127 | },
128 | "widgets_values": [
129 | "",
130 | "upper_body",
131 | false,
132 | false,
133 | false,
134 | 30,
135 | 71,
136 | "randomize",
137 | false
138 | ]
139 | },
140 | {
141 | "id": 4,
142 | "type": "SaveImage",
143 | "pos": [
144 | 1312,
145 | 296
146 | ],
147 | "size": [
148 | 368.7568359375,
149 | 308.6533203125
150 | ],
151 | "flags": {},
152 | "order": 3,
153 | "mode": 0,
154 | "inputs": [
155 | {
156 | "name": "images",
157 | "type": "IMAGE",
158 | "link": 3
159 | }
160 | ],
161 | "properties": {},
162 | "widgets_values": [
163 | "ComfyUI"
164 | ]
165 | }
166 | ],
167 | "links": [
168 | [
169 | 1,
170 | 3,
171 | 0,
172 | 1,
173 | 1,
174 | "IMAGE"
175 | ],
176 | [
177 | 2,
178 | 2,
179 | 0,
180 | 1,
181 | 0,
182 | "IMAGE"
183 | ],
184 | [
185 | 3,
186 | 1,
187 | 0,
188 | 4,
189 | 0,
190 | "IMAGE"
191 | ]
192 | ],
193 | "groups": [],
194 | "config": {},
195 | "extra": {
196 | "ds": {
197 | "scale": 1,
198 | "offset": [
199 | 33.1787109375,
200 | -26.455078125
201 | ]
202 | }
203 | },
204 | "version": 0.4
205 | }
--------------------------------------------------------------------------------
/flowy/nodes.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import logging
4 | from .types import STRING
5 | from .api_key_manager import save_api_key
6 |
7 | # 设置日志
8 | logging.basicConfig(level=logging.DEBUG)
9 | logger = logging.getLogger(__name__)
10 |
11 | from .nodes_omost import (
12 | OmostLLMNode,
13 | OmostToConditioning,
14 | ComflowyOmostPreviewNode,
15 | ComflowyOmostLoadCanvasPythonCodeNode,
16 | ComflowyOmostLoadCanvasConditioningNode,
17 | )
18 |
19 | from .nodes_json import FlowyPreviewJSON, FlowyExtractJSON, ComflowyLoadJSON
20 | from .nodes_http import FlowyHttpRequest
21 | from .nodes_llm import FlowyLLM
22 | from .api_nodes import (
23 | FlowyClarityUpscale,
24 | FlowyFlux,
25 | FlowyFluxProUltra,
26 | FlowyFluxDevLora,
27 | FlowyHailuo,
28 | FlowyIdeogram,
29 | FlowyKling,
30 | FlowyLuma,
31 | FlowyRecraft,
32 | REPLICATE_NODE_CLASS_MAPPINGS,
33 | )
34 |
35 | from .nodes_previewvideo import PreviewVideo
36 |
37 | API_KEY_FILE = os.path.join(os.path.dirname(__file__), "api_key.json")
38 |
39 | class ComflowySetAPIKey:
40 | """
41 | A node for setting the global Comflowy API Key.
42 | """
43 | @classmethod
44 | def INPUT_TYPES(s):
45 | return {"required": {"api_key": STRING}}
46 |
47 | RETURN_TYPES = ()
48 | FUNCTION = "set_api_key"
49 | OUTPUT_NODE = True
50 | CATEGORY = "Comflowy"
51 |
52 | def set_api_key(self, api_key):
53 | """
54 | Set the global API key for Comflowy.
55 |
56 | Args:
57 | api_key (str): The API key to be set.
58 |
59 | Returns:
60 | tuple: An empty tuple as this node doesn't produce any output.
61 |
62 | Raises:
63 | ValueError: If the provided API key is empty.
64 | """
65 | if not api_key.strip():
66 | raise ValueError("API Key cannot be empty")
67 | save_api_key(api_key)
68 | print("Comflowy API Key has been set globally")
69 | return ()
70 |
71 | NODE_CLASS_MAPPINGS = {
72 | "Comflowy_Http_Request": FlowyHttpRequest,
73 | "Comflowy_LLM": FlowyLLM,
74 | "Comflowy_Preview_JSON": FlowyPreviewJSON,
75 | "Comflowy_Extract_JSON": FlowyExtractJSON,
76 | "Comflowy_Load_JSON": ComflowyLoadJSON,
77 | "Comflowy_Omost_LLM": OmostLLMNode,
78 | "Comflowy_Omost_To_Conditioning": OmostToConditioning,
79 | "Comflowy_Omost_Preview": ComflowyOmostPreviewNode,
80 | "Comflowy_Omost_Load_Canvas_Python_Code": ComflowyOmostLoadCanvasPythonCodeNode,
81 | "Comflowy_Omost_Load_Canvas_Conditioning": ComflowyOmostLoadCanvasConditioningNode,
82 | "Comflowy_Set_API_Key": ComflowySetAPIKey,
83 | "Comflowy_Clarity_Upscale": FlowyClarityUpscale,
84 | "Comflowy_Ideogram": FlowyIdeogram,
85 | "Comflowy_Flux": FlowyFlux,
86 | "Comflowy_Recraft": FlowyRecraft,
87 | "Comflowy_Hailuo": FlowyHailuo,
88 | "Comflowy_Preview_Video": PreviewVideo,
89 | "Comflowy_Luma": FlowyLuma,
90 | "Comflowy_Kling": FlowyKling,
91 | "Comflowy_Flux_Pro_Ultra": FlowyFluxProUltra,
92 | "Comflowy_Flux_Dev_Lora": FlowyFluxDevLora,
93 | **REPLICATE_NODE_CLASS_MAPPINGS
94 | }
95 |
96 |
97 | NODE_DISPLAY_NAME_MAPPINGS = {
98 | "Comflowy_Http_Request": "Comflowy Http Request",
99 | "Comflowy_LLM": "Comflowy LLM",
100 | "Comflowy_Preview_JSON": "Comflowy Preview JSON",
101 | "Comflowy_Extract_JSON": "Comflowy Extract JSON",
102 | "Comflowy_Load_JSON": "Comflowy Load JSON",
103 | "Comflowy_Omost_LLM": "Comflowy Omost LLM",
104 | "Comflowy_Omost_To_Conditioning": "Comflowy Omost To Conditioning",
105 | "Comflowy_Omost_Preview": "Comflowy Omost Preview",
106 | "Comflowy_Omost_Load_Canvas_Python_Code": "Comflowy Omost Load Canvas Python Code",
107 | "Comflowy_Omost_Load_Canvas_Conditioning": "Comflowy Omost Load Canvas Conditioning",
108 | "Comflowy_Set_API_Key": "Comflowy Set API Key",
109 | "Comflowy_Clarity_Upscale": "Comflowy Clarity Upscale",
110 | "Comflowy_Ideogram": "Comflowy Ideogram",
111 | "Comflowy_Flux": "Comflowy Flux",
112 | "Comflowy_Recraft": "Comflowy Recraft",
113 | "Comflowy_Hailuo": "Comflowy Hailuo",
114 | "Comflowy_Preview_Video": "Comflowy Preview Video",
115 | "Comflowy_Luma": "Comflowy Luma",
116 | "Comflowy_Kling": "Comflowy Kling",
117 | "Comflowy_Flux_Pro_Ultra": "Comflowy Flux Pro Ultra",
118 | "Comflowy_Flux_Dev_Lora": "Comflowy Flux Dev Lora",
119 | }
120 |
--------------------------------------------------------------------------------
/flowy/api_nodes/replicate/replicate_bridge.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import time
4 |
5 | import torch
6 | from ..base import FlowyApiNode
7 | from .schema_to_node import (
8 | schema_to_comfyui_input_types,
9 | get_return_type,
10 | name_and_version,
11 | inputs_that_need_arrays,
12 | )
13 | from ...api_key_manager import load_api_key
14 |
15 | def create_comfyui_node(schema):
16 | replicate_model, node_name = name_and_version(schema)
17 | return_type = get_return_type(schema)
18 |
19 | class ReplicateNode(FlowyApiNode):
20 | @classmethod
21 | def IS_CHANGED(cls, **kwargs):
22 | return time.time() if kwargs["force_rerun"] else ""
23 |
24 | @classmethod
25 | def INPUT_TYPES(cls):
26 | return schema_to_comfyui_input_types(schema)
27 |
28 | RETURN_TYPES = (
29 | tuple(return_type.values())
30 | if isinstance(return_type, dict)
31 | else (return_type,)
32 | )
33 | CATEGORY = "Comflowy Replicate"
34 |
35 | def get_model_type(self) -> str:
36 | return "replicate"
37 |
38 | def get_api_key(self) -> str:
39 | """获取 Comflowy API token"""
40 | api_key = load_api_key()
41 | if not api_key:
42 | raise ValueError("Comflowy API key not found. Please set your API key first.")
43 | return api_key
44 |
45 | def prepare_payload(self, **kwargs):
46 | # Remove force_rerun from kwargs
47 | kwargs = {k: v for k, v in kwargs.items() if k != "force_rerun"}
48 |
49 | # Handle array inputs
50 | array_inputs = inputs_that_need_arrays(schema)
51 | for input_name in array_inputs:
52 | if input_name in kwargs:
53 | if isinstance(kwargs[input_name], str):
54 | kwargs[input_name] = (
55 | []
56 | if kwargs[input_name] == ""
57 | else kwargs[input_name].split("\n")
58 | )
59 | else:
60 | kwargs[input_name] = [kwargs[input_name]]
61 |
62 | # Process image and audio inputs
63 | for key, value in kwargs.items():
64 | if value is not None:
65 | input_type = (
66 | self.INPUT_TYPES()["required"].get(key, (None,))[0]
67 | or self.INPUT_TYPES().get("optional", {}).get(key, (None,))[0]
68 | )
69 | if input_type == "IMAGE":
70 | kwargs[key] = self.image_to_base64(value)
71 | elif input_type == "AUDIO":
72 | kwargs[key] = self.audio_to_base64(value)
73 |
74 | # Remove empty optional inputs
75 | optional_inputs = self.INPUT_TYPES().get("optional", {})
76 | for key in list(kwargs.keys()):
77 | if key in optional_inputs:
78 | if isinstance(kwargs[key], torch.Tensor):
79 | continue
80 | elif not kwargs[key]:
81 | del kwargs[key]
82 |
83 | # 添加 API token 到 payload
84 | return {
85 | "replicate_model": replicate_model,
86 | "input": kwargs,
87 | "api_key": self.get_api_key() # 添加 API token
88 | }
89 |
90 | return node_name, ReplicateNode
91 |
92 |
93 | def create_comfyui_nodes_from_schemas(schemas_dir):
94 | nodes = {}
95 | current_path = os.path.dirname(os.path.abspath(__file__))
96 | schemas_dir_path = os.path.join(current_path, schemas_dir)
97 | for schema_file in os.listdir(schemas_dir_path):
98 | if schema_file.endswith(".json"):
99 | with open(
100 | os.path.join(schemas_dir_path, schema_file), "r", encoding="utf-8"
101 | ) as f:
102 | schema = json.load(f)
103 | node_name, node_class = create_comfyui_node(schema)
104 | nodes[node_name] = node_class
105 | return nodes
106 |
107 |
108 | _cached_node_class_mappings = None
109 |
110 | def get_node_class_mappings():
111 | global _cached_node_class_mappings
112 | if _cached_node_class_mappings is None:
113 | _cached_node_class_mappings = create_comfyui_nodes_from_schemas("schemas")
114 | return _cached_node_class_mappings
115 |
116 |
117 | REPLICATE_NODE_CLASS_MAPPINGS = get_node_class_mappings()
118 |
--------------------------------------------------------------------------------
/flowy/lib_omost/greedy_encode.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | import logging
3 | import torch
4 | from typing import Callable, NamedTuple, TypedDict
5 |
6 |
7 | class SpecialTokens(TypedDict):
8 | start: int
9 | end: int
10 | pad: int
11 |
12 |
13 | class CLIPTokens(NamedTuple):
14 | clip_l_tokens: list[int]
15 | clip_g_tokens: list[int] | None = None
16 |
17 | @classmethod
18 | def empty_tokens(cls) -> CLIPTokens:
19 | return CLIPTokens(clip_l_tokens=[], clip_g_tokens=[])
20 |
21 | @property
22 | def length(self) -> int:
23 | return len(self.clip_l_tokens)
24 |
25 | def __repr__(self) -> str:
26 | return f"CLIPTokens(clip_l_tokens({len(self.clip_l_tokens)}), clip_g_tokens={len(self.clip_g_tokens) if self.clip_g_tokens else None})"
27 |
28 | def __add__(self, other: CLIPTokens) -> CLIPTokens:
29 | if self.clip_g_tokens is None or other.clip_g_tokens is None:
30 | clip_g_tokens = None
31 | else:
32 | clip_g_tokens = self.clip_g_tokens + other.clip_g_tokens
33 |
34 | return CLIPTokens(
35 | clip_l_tokens=self.clip_l_tokens + other.clip_l_tokens,
36 | clip_g_tokens=clip_g_tokens,
37 | )
38 |
39 | @staticmethod
40 | def _get_77_tokens(subprompt_inds: list[int]) -> list[int]:
41 | # Note that all subprompt are theoretically less than 75 tokens (without bos/eos)
42 | result = (
43 | [SPECIAL_TOKENS["start"]]
44 | + subprompt_inds[:75]
45 | + [SPECIAL_TOKENS["end"]]
46 | + [SPECIAL_TOKENS["pad"]] * 75
47 | )
48 | return result[:77]
49 |
50 | def clamp_to_77_tokens(self) -> CLIPTokens:
51 | return CLIPTokens(
52 | clip_l_tokens=self._get_77_tokens(self.clip_l_tokens),
53 | clip_g_tokens=(
54 | self._get_77_tokens(self.clip_g_tokens)
55 | if self.clip_g_tokens
56 | else None
57 | ),
58 | )
59 |
60 |
61 | class EncoderOutput(NamedTuple):
62 | cond: torch.Tensor
63 | pooler: torch.Tensor
64 |
65 |
66 | TokenizeFunc = Callable[[str], CLIPTokens]
67 | EncodeFunc = Callable[[CLIPTokens], EncoderOutput]
68 |
69 |
70 | # ComfyUI protocol. See sd1_clip.py/sdxl_clip.py for the actual implementation.
71 | SPECIAL_TOKENS: SpecialTokens = {"start": 49406, "end": 49407, "pad": 49407}
72 |
73 |
74 | def greedy_partition(items: list[CLIPTokens], max_sum: int) -> list[list[CLIPTokens]]:
75 | bags: list[list[CLIPTokens]] = []
76 | current_bag: list[CLIPTokens] = []
77 | current_sum: int = 0
78 |
79 | for item in items:
80 | num = item.length
81 | if current_sum + num > max_sum:
82 | if current_bag:
83 | bags.append(current_bag)
84 | current_bag = [item]
85 | current_sum = num
86 | else:
87 | current_bag.append(item)
88 | current_sum += num
89 |
90 | if current_bag:
91 | bags.append(current_bag)
92 |
93 | return bags
94 |
95 |
96 | def encode_bag_of_subprompts_greedy(
97 | prefixes: list[str],
98 | suffixes: list[str],
99 | tokenize_func: TokenizeFunc,
100 | encode_func: EncodeFunc,
101 | logger: logging.Logger | None = None,
102 | ) -> EncoderOutput:
103 | """
104 | Note: tokenize_func is expected to clamp the tokens to 75 tokens.
105 | """
106 | if logger is None:
107 | logger = logging.getLogger(__name__)
108 |
109 | # Begin with tokenizing prefixes
110 | prefix_tokens: CLIPTokens = sum(
111 | [tokenize_func(prefix) for prefix in prefixes], CLIPTokens.empty_tokens()
112 | )
113 | logger.debug(f"Prefix tokens: {prefix_tokens}")
114 |
115 | # Then tokenizing suffixes
116 | allowed_suffix_length = 75 - prefix_tokens.length
117 | logger.debug(f"Allowed suffix length: {allowed_suffix_length}")
118 | suffix_targets: list[CLIPTokens] = [
119 | tokenize_func(subprompt) for subprompt in suffixes
120 | ]
121 | logger.debug(f"Suffix targets: {suffix_targets}")
122 |
123 | # Then merge prefix and suffix tokens
124 | suffix_targets = greedy_partition(suffix_targets, max_sum=allowed_suffix_length)
125 | targets = [
126 | sum([prefix_tokens, *b], CLIPTokens.empty_tokens()).clamp_to_77_tokens()
127 | for b in suffix_targets
128 | ]
129 |
130 | # Encode!
131 | encoded_embeds = [encode_func(target) for target in targets]
132 | conds_merged = torch.concat([embed.cond for embed in encoded_embeds], dim=1)
133 | poolers_merged = encoded_embeds[0].pooler
134 | logger.debug(f"merged conds: {conds_merged.shape}, pooler: {poolers_merged.shape}")
135 |
136 | return EncoderOutput(cond=conds_merged, pooler=poolers_merged)
137 |
--------------------------------------------------------------------------------
/flowy/nodes_json.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Tuple
3 |
4 | # You can use this node to save full size images through the websocket, the
5 | # images will be sent in exactly the same format as the image previews: as
6 | # binary images on the websocket with a 8 byte header indicating the type
7 | # of binary message (first 4 bytes) and the image format (next 4 bytes).
8 |
9 | # Note that no metadata will be put in the images saved with this node.
10 | from .types import (
11 | STRING,
12 | )
13 | from .utils import get_nested_value, logger
14 |
15 | class FlowyExtractJSON:
16 | @classmethod
17 | def INPUT_TYPES(s):
18 | return {
19 | "required": {"json_value": ("JSON",)},
20 | "optional": {
21 | "json_path1": STRING,
22 | "json_path2": STRING,
23 | "json_path3": STRING,
24 | "json_path4": STRING,
25 | "json_path5": STRING,
26 | },
27 | }
28 |
29 | CATEGORY = "Comflowy"
30 | RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
31 | RETURN_NAMES = ("text1", "text2", "text3", "text4", "text5")
32 | OUTPUT_NODE = True
33 | FUNCTION = "extract_json"
34 | DESCRIPTION = """
35 | Nodes from https://comflowy.com:
36 | - Description: Extract values from a JSON object.
37 | - How to use: Provide a JSON object and the json_path to extract the value.
38 | - eg1. json_path: "a.b.c"
39 | - eg2. json_path: "outputs.0.text" if there is an array in the json object.
40 | - Output: All output values are strings, according to the provided `json_path`, if the target json_path is not a string, will return the json dump value.
41 | - Note: If the json_path is not found, it will return an error string.
42 | """
43 |
44 | def extract_json(
45 | self,
46 | json_value=None,
47 | json_path1=None,
48 | json_path2=None,
49 | json_path3=None,
50 | json_path4=None,
51 | json_path5=None,
52 | ):
53 | ret = ["", "", "", "", ""]
54 | paths = [json_path1, json_path2, json_path3, json_path4, json_path5]
55 | # return {"ui": {"text": [text]}, "result": (text,)}
56 | if json_value is not None:
57 | for i, path in enumerate(paths):
58 | if path:
59 | try:
60 | ret[i] = json.dumps(
61 | get_nested_value(json_value, path), indent=4
62 | )
63 | except Exception as e:
64 | ret[i] = f"Extract error: {e}"
65 | logger.warn(e)
66 |
67 | logger.info(f"Extract json is running: {ret}")
68 | all_text = "\n".join(ret)
69 | return {"ui": {"text": [all_text]}, "result": tuple(ret)}
70 |
71 |
72 | class FlowyPreviewJSON:
73 |
74 | @classmethod
75 | def INPUT_TYPES(s):
76 | return {
77 | "required": {"json_value": ("JSON",)},
78 | }
79 |
80 | CATEGORY = "Comflowy"
81 | RETURN_TYPES = ("STRING",)
82 | OUTPUT_NODE = True
83 | FUNCTION = "preview_json"
84 | DESCRIPTION = """
85 | Nodes from https://comflowy.com:
86 | - Description: Show a JSON in a human-readable format.
87 | - Output: Return a JSON object as a string.
88 | """
89 |
90 | def preview_json(self, json_value=None):
91 | text = ""
92 | logger.info(f"preview json", json_value)
93 | # return {"ui": {"text": [text]}, "result": (text,)}
94 | if json_value is not None:
95 | if isinstance(json_value, dict):
96 | try:
97 | text = json.dumps(json_value, indent=4)
98 | except Exception as e:
99 | text = "The input is a dict, but could not be serialized.\n"
100 | logger.warn(e)
101 |
102 | elif isinstance(json_value, list):
103 | try:
104 | text = json.dumps(json_value, indent=4)
105 | except Exception as e:
106 | text = "The input is a list, but could not be serialized.\n"
107 | logger.warn(e)
108 |
109 | else:
110 | text = str(json_value)
111 |
112 | return {"ui": {"text": [text]}, "result": (text,)}
113 |
114 |
115 | class ComflowyLoadJSON:
116 | @classmethod
117 | def INPUT_TYPES(s):
118 | return {
119 | "required": {
120 | "json_str": ("STRING", {"multiline": True}),
121 | }
122 | }
123 |
124 | RETURN_TYPES = ("JSON",)
125 | FUNCTION = "load_json"
126 | CATEGORY = "Comflowy"
127 |
128 | def load_json(self, json_str: str) -> Tuple[list[any]]:
129 | """Load canvas from file"""
130 | return (json.loads(json_str),)
131 |
--------------------------------------------------------------------------------
/flowy/api_nodes/clarityupscaler.py:
--------------------------------------------------------------------------------
1 | from .base import FlowyApiNode
2 | from ..types import STRING, INT, get_api_host
3 |
4 | class FlowyClarityUpscale(FlowyApiNode):
5 | @classmethod
6 | def INPUT_TYPES(cls):
7 | return {
8 | "required": {
9 | "image": ("IMAGE",),
10 | "scale_factor": (
11 | "FLOAT",
12 | {"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.1},
13 | ),
14 | "dynamic": (
15 | "FLOAT",
16 | {"default": 6.0, "min": 1.0, "max": 50.0, "step": 0.1},
17 | ),
18 | "creativity": (
19 | "FLOAT",
20 | {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01},
21 | ),
22 | "resemblance": (
23 | "FLOAT",
24 | {"default": 0.6, "min": 0.0, "max": 3.0, "step": 0.01},
25 | ),
26 | "tiling_width": ( [ 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, ], {"default": 112}, ),
27 | "tiling_height": ( [ 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, ], {"default": 144}, ),
28 | "num_inference_steps": ("INT", {"default": 18, "min": 1, "max": 100}),
29 | "seed": ("INT", {"default": 1337, "min": 0, "max": 2147483647}),
30 | "handfix": (
31 | ["disabled", "hands_only", "image_and_hands"],
32 | {"default": "disabled"},
33 | ),
34 | "pattern": ("BOOLEAN", {"default": False}),
35 | "sharpen": (
36 | "FLOAT",
37 | {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1},
38 | ),
39 | "downscaling": ("BOOLEAN", {"default": False}),
40 | "downscaling_resolution": (
41 | "INT",
42 | {"default": 768, "min": 256, "max": 2048},
43 | ),
44 | "sd_model": (
45 | [
46 | "epicrealism_naturalSinRC1VAE.safetensors [84d76a0328]",
47 | "juggernaut_reborn.safetensors [338b85bc4f]",
48 | "flat2DAnimerge_v45Sharp.safetensors",
49 | ],
50 | {"default": "juggernaut_reborn.safetensors [338b85bc4f]"},
51 | ),
52 | "scheduler": ( [ "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM++ 2M SDE Exponential", "DPM++ 2M SDE Karras", "Euler a", "Euler", "LMS", "Heun", "DPM2", "DPM2 a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "DPM++ 2M SDE Heun", "DPM++ 2M SDE Heun Karras", "DPM++ 2M SDE Heun Exponential", "DPM++ 3M SDE", "DPM++ 3M SDE Karras", "DPM++ 3M SDE Exponential", "DPM fast", "DPM adaptive", "LMS Karras", "DPM2 Karras", "DPM2 a Karras", "DPM++ 2S a Karras", "Restart", "DDIM", "PLMS", "UniPC", ], {"default": "DPM++ 3M SDE Karras"}, ),
53 | }
54 | }
55 |
56 | RETURN_TYPES = ("IMAGE",)
57 | FUNCTION = "generate" # Changed from upscale to match parent class
58 | DESCRIPTION = """
59 | Nodes from https://comflowy.com:
60 | - Description: A service to upscale images using AI models.
61 | - How to use:
62 | - Provide an image to upscale.
63 | - Dynamic: HDR, try from 3 - 9.
64 | - Pattern: Upscale a pattern with seamless tiling.
65 | - Creativity: Try from 0.3 - 0.9.
66 | - Downscaling: Downscale the image before upscaling. Can improve quality and speed for images with high resolution but lower quality.
67 | - Resemblance: Try from 0.3 - 1.6.
68 | - Make sure to set your API Key using the 'Comflowy Set API Key' node before using this node.
69 | - Output: Returns the upscaled image.
70 | """
71 |
72 | def get_model_type(self) -> str:
73 | return "clarityupscaler"
74 |
75 | def get_api_host(self) -> str:
76 | API_HOST = get_api_host()
77 | return f"{API_HOST}/api/open/v0/clarityupscaler"
78 |
79 | def prepare_payload(self, **kwargs) -> dict:
80 | image_base64 = self.image_to_base64(kwargs["image"])
81 | return {
82 | "image": image_base64,
83 | "scale_factor": kwargs["scale_factor"],
84 | "dynamic": kwargs["dynamic"],
85 | "creativity": kwargs["creativity"],
86 | "resemblance": kwargs["resemblance"],
87 | "tiling_width": kwargs["tiling_width"],
88 | "tiling_height": kwargs["tiling_height"],
89 | "num_inference_steps": kwargs["num_inference_steps"],
90 | "seed": kwargs["seed"],
91 | "handfix": kwargs["handfix"],
92 | "pattern": kwargs["pattern"],
93 | "sharpen": kwargs["sharpen"],
94 | "downscaling": kwargs["downscaling"],
95 | "downscaling_resolution": kwargs["downscaling_resolution"],
96 | "sd_model": kwargs["sd_model"],
97 | "scheduler": kwargs["scheduler"],
98 | }
99 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Comflowy 插件
4 |
5 |
2 |
3 | # Comflowy ComfyUI Extension
4 |
5 |