├── .github ├── auto-assign.yml └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── comfyui_utils ├── __init__.py ├── comfy.py ├── gen_prompts.py └── test_gen_prompts.py ├── examples ├── __init__.py ├── e2e.py └── workflows │ └── sdxl.json ├── pyproject.toml ├── requirements.txt └── setup.py /.github/auto-assign.yml: -------------------------------------------------------------------------------- 1 | assignees: 2 | - andreyryabtsev 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | branches: 8 | - "main" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.10", "3.11"] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -r requirements.txt 26 | pip install -e . 27 | pip install pytest 28 | - name: Test 29 | run: | 30 | pytest 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache/ 2 | __pycache__/ 3 | comfyui_utils.egg-info/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI utils 2 | 3 | This package provides simple utils for: 4 | 1. Parsing out prompt arguments, e.g. "a beautiful forest $num_steps=12" 5 | 2. Running a workflow in parsed API format against a ComfyUI endpoint, with callbacks for specified events. 6 | 7 | It's designed primarily for developing casual chatbots (e.g. a Discord bot) where users can adjust certain parameters and receive live progress updates. 8 | 9 | Limitations: 10 | - Only integer arguments are currently supported in addition to the prompt itself. The plan is to add at least floats and strings. 11 | - Only one output from the workflow is supported. 12 | 13 | Supports: 14 | - Arbitrary number of integer args embedded in the main string prompt. 15 | - Queuing with a callback when the queue position changes. 16 | - Fetching cached results. 17 | - Reporting intermediate progress of nodes like `KSampler`. 18 | 19 | 20 | ## Install 21 | 22 | ``` 23 | pip install comfyui_utils 24 | ``` 25 | 26 | ## Usage 27 | 28 | (better docs are coming, for now please look at the source code / sample script) 29 | 30 | ``` 31 | from comfyui_utils import gen_prompts, comfy 32 | gen_prompts.make_config("GenVid", [gen_prompts.IntArg("num_steps", default_value=12, min_value=1, max_value=80)]) 33 | ... 34 | try: 35 | parsed = gen_prompts.parse_args(raw_prompt, prompt_config) 36 | except ValueError as e: 37 | print(f"Invalid prompt {e.args[0]}") 38 | prompt_data = ... 39 | class Callbacks(comfy.Callbacks): 40 | ... 41 | await comfyui.submit(prompt_data, Callbacks()) 42 | def on_load(data_buffer): 43 | ... 44 | await comfyui.fetch(backend_filepath, on_load) 45 | ``` 46 | 47 | ## Example 48 | 49 | To test the library with a sample SDXL workflow, run the following after installing (replace the address with your ComfyUI endpoint). Make sure your ComfyUI has `sd_xl_base_1.0.safetensors` and `sd_xl_refiner_1.0.safetensors` installed (or replace the workflow). 50 | 51 | ```python 52 | comfy_ui_example_e2e\ 53 | --address='192.168.0.10:11010'\ 54 | --prompt='a smiling potato $base_steps=8$refiner_steps=3'\ 55 | --output='./potato.png' 56 | ``` 57 | The single quotes are important so your shell doesn't try to parse the `$`'s. Expected output: 58 | ``` 59 | Queuing workflow. 60 | Queue position: #0 61 | Base... 62 | Base: 1/8 63 | Base: 2/8 64 | Base: 3/8 65 | Base: 4/8 66 | Base: 5/8 67 | Base: 6/8 68 | Base: 7/8 69 | Base: 8/8 70 | Refiner... 71 | Refiner: 1/3 72 | Refiner: 2/3 73 | Refiner: 3/3 74 | Decoding... 75 | Saving image on backend... 76 | Result (cached: no): 77 | {'images': [{'filename': 'ComfyUI_00101_.png', 'subfolder': '', 'type': 'output'}]} 78 | ``` 79 | The file will be saved in the root directory. 80 | 81 | ## Use your own workflow 82 | 83 | After finalizing the workflow, use the "Save (API format)" button to store the workflow. Then, edit the `PromptConfig` in the script to reflect the arguments you wish to make available, and ensure the prompt has them replaced after parsing. 84 | -------------------------------------------------------------------------------- /comfyui_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreyryabtsev/comfyui-python-api/56c4364c12ddaa4512ee95c53f04db0b497b7a14/comfyui_utils/__init__.py -------------------------------------------------------------------------------- /comfyui_utils/comfy.py: -------------------------------------------------------------------------------- 1 | """ 2 | A wrapper for the comfyUI's API to provide convenient event callbacks during workflow execution. 3 | 4 | Known limitations: 5 | - multiple output nodes untested, probably won't work 6 | """ 7 | 8 | import abc 9 | import dataclasses 10 | import io 11 | import json 12 | import logging 13 | from typing import Any, Callable, Union 14 | import uuid 15 | 16 | import aiohttp 17 | 18 | 19 | # Inherit this class to specify callbacks during prompt execution. 20 | class Callbacks(abc.ABC): 21 | @abc.abstractmethod 22 | async def queue_position(self, position: str): 23 | """Called when the prompt's queue position updates, with the position in the queue (0 = already being executed).""" 24 | @abc.abstractmethod 25 | async def in_progress(self, node_id: int, progress: int, total: int): 26 | """Called when a node is in progress, with current and total steps.""" 27 | @abc.abstractmethod 28 | async def completed(self, outputs: dict[str, Any], cached: bool): 29 | """Called when the prompt completes, with the final output.""" 30 | 31 | 32 | StrDict = dict[str, Any] # parsed JSON of an API-formatted ComfyUI workflow. 33 | 34 | 35 | def _parse_queue(queue_json): 36 | """Returns a list of prompt IDs in the queue, the 0th (if present) element is currently executed.""" 37 | assert len(queue_json["queue_running"]) <= 1 38 | result = [] 39 | if queue_json["queue_running"]: 40 | result.append(queue_json["queue_running"][0][1]) 41 | for pending in queue_json["queue_pending"]: 42 | result.append(pending[1]) 43 | return result 44 | 45 | 46 | def _find_prompt_in_history(history, prompt): 47 | for prompt_id, data in history.items(): 48 | original_prompt = data["prompt"][2] 49 | if original_prompt == prompt: 50 | return prompt_id 51 | return None 52 | 53 | 54 | @dataclasses.dataclass 55 | class PromptSession: 56 | client_id: str 57 | prompt_id: str 58 | prompt: StrDict 59 | session: aiohttp.ClientSession 60 | address: str 61 | 62 | 63 | async def _get_queue_position_or_cached_result(sess: PromptSession) -> Union[int, StrDict]: 64 | """Returns """ 65 | async with sess.session.get(f"http://{sess.address}/queue") as queue_resp: 66 | queue = await queue_resp.json() 67 | queue = _parse_queue(queue) 68 | # logging.warning("QUEUE: %s", queue) 69 | if sess.prompt_id in queue: # Prompt is queued. 70 | return queue.index(sess.prompt_id) 71 | # Prompt is cached, so not queued. Have to fetch output info from history. 72 | async with sess.session.get(f"http://{sess.address}/history") as history_resp: 73 | history = await history_resp.json() 74 | cached_id = _find_prompt_in_history(history, sess.prompt) 75 | if cached_id is None: 76 | raise ValueError("Response seems cached, but not found in history!") 77 | cached_outputs = history[cached_id]["outputs"] 78 | # TODO: support multiple outputs here and in the un-cached case. 79 | return cached_outputs[next(iter(cached_outputs))] 80 | 81 | 82 | 83 | async def _prompt_websocket(sess: PromptSession, callbacks: Callbacks) -> None: 84 | """Connects to a websocket on the given address/session and invokes callbacks to handle prompt execution.""" 85 | async with sess.session.ws_connect(f"ws://{sess.address}/ws?clientId={sess.client_id}") as ws: 86 | current_node = None 87 | async for msg in ws: 88 | # logging.warning(msg) 89 | if msg.type == aiohttp.WSMsgType.ERROR: 90 | raise BrokenPipeError(f"WebSocket error: {msg.data}") 91 | assert msg.type == aiohttp.WSMsgType.TEXT 92 | message = json.loads(msg.data) 93 | # Handle prompt being started. 94 | if message["type"] == "status": 95 | queue_or_result = await _get_queue_position_or_cached_result(sess) 96 | if isinstance(queue_or_result, int): 97 | await callbacks.queue_position(queue_or_result) 98 | else: 99 | await callbacks.completed(queue_or_result, True) 100 | break 101 | # Handle a node being executed. 102 | if message["type"] == "executing": 103 | if message["data"]["node"] is not None: 104 | node_id = int(message["data"]["node"]) 105 | current_node = node_id 106 | await callbacks.in_progress(current_node, 0, 0) 107 | # Handle completion of the request. 108 | if message["type"] == "executed": 109 | assert message["data"]["prompt_id"] == sess.prompt_id 110 | await callbacks.completed(message["data"]["output"], False) 111 | break 112 | # Handle progress on a node. 113 | if message["type"] == "progress": 114 | progress = int(message["data"]["value"]) 115 | total = int(message["data"]["max"]) 116 | await callbacks.in_progress(current_node, progress, total) 117 | 118 | 119 | class ComfyAPI: 120 | def __init__(self, address): 121 | self.address = address 122 | 123 | async def fetch(self, filename: str, callback: Callable[[io.BytesIO], None]): 124 | """Fetch a generated piece of data from Comfy. 125 | Invokes callback with an io.BytesIO object.""" 126 | async with aiohttp.ClientSession() as session: 127 | async with session.get(f"http://{self.address}/view", params=filename) as resp: 128 | data = await resp.read() 129 | with io.BytesIO(data) as data_file: 130 | await callback(data_file) 131 | 132 | async def submit(self, prompt: StrDict, callbacks: Callbacks): 133 | client_id = str(uuid.uuid4()) 134 | init_data = json.dumps({ 135 | "prompt": prompt, 136 | "client_id": client_id 137 | }).encode('utf-8') 138 | async with aiohttp.ClientSession() as session: 139 | # Enqueue and get prompt ID. 140 | async with session.post(f"http://{self.address}/prompt", data=init_data) as resp: 141 | response_json = await resp.json() 142 | logging.info(response_json) 143 | if "error" in response_json: 144 | if "node_errors" not in response_json: 145 | raise ValueError(response_json["error"]["message"]) 146 | errors = [] 147 | for node_id, data in response_json["node_errors"].items(): 148 | for node_error in data["errors"]: 149 | errors.append(f"Node {node_id}, {node_error['details']}: {node_error['message']}") 150 | raise ValueError("\n" + "\n".join(errors)) 151 | 152 | prompt_id = response_json['prompt_id'] 153 | # Listen on a websocket until the prompt completes and invoke callbacks. 154 | await _prompt_websocket(PromptSession( 155 | client_id=client_id, 156 | prompt_id=prompt_id, 157 | prompt=prompt, 158 | session=session, 159 | address=self.address 160 | ), callbacks) 161 | -------------------------------------------------------------------------------- /comfyui_utils/gen_prompts.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import dataclasses 3 | import functools 4 | import re 5 | from typing import Any, Generic, TypeVar, Union, get_args 6 | 7 | _ArgType = Union[int, str] 8 | T = TypeVar("T", bound=_ArgType) 9 | Warnings = list[str] 10 | 11 | @dataclasses.dataclass 12 | class _PromptArg(abc.ABC, Generic[T]): 13 | name: str 14 | @classmethod 15 | def get_type(cls) -> T: 16 | return get_args(cls.__orig_bases__[0])[0] 17 | 18 | @abc.abstractmethod 19 | def parse(self, raw: T) -> [T, Warnings]: 20 | """Returns the parsed value and a possibly empty list of warnings."""\ 21 | 22 | 23 | @dataclasses.dataclass 24 | class IntArg(_PromptArg[int]): 25 | default_value: int 26 | min_value: int | None = None 27 | max_value: int | None = None 28 | 29 | def parse(self, raw: str) -> tuple[int, Warnings]: 30 | try: 31 | value = int(raw) 32 | except ValueError as exc: 33 | raise ValueError(f"Invalid argument {self.name}: must be integer") from exc 34 | if self.min_value is not None and value < self.min_value: 35 | return self.min_value, [f"{self.name} {value} too low, defaulting to {self.min_value}"] 36 | if self.max_value is not None and value > self.max_value: 37 | return self.max_value, [f"{self.name} {value} too high, defaulting to {self.max_value}"] 38 | return value, [] 39 | 40 | 41 | @dataclasses.dataclass 42 | class Config: 43 | _result_type: type 44 | _arg_list: list[str] 45 | _config: list[_PromptArg] 46 | 47 | 48 | def make_config(name: str, config: list[_PromptArg]) -> Config: 49 | result_dc = dataclasses.make_dataclass(f"{name}Result", [ 50 | (arg.name, arg.get_type(), dataclasses.field(default=arg.default_value)) 51 | for arg in config 52 | ]) 53 | arg_list = [arg.name for arg in config] 54 | return Config(result_dc, arg_list, config) 55 | 56 | 57 | @dataclasses.dataclass 58 | class ParsedPrompt: 59 | cleaned: str 60 | result: Any 61 | warnings: list[str] 62 | 63 | 64 | _REGEX_TEMPLATE = r'\$ARGNAME=([^\s\$]*)' 65 | _REGEX_CATCHALL = r'\$([^\s]*)=([^\s\$]*)' 66 | def _regex(name: str): 67 | return _REGEX_TEMPLATE.replace("ARGNAME", name) 68 | def _leftover_args(parsed: str): 69 | return [ 70 | f"{kv[0]}={kv[1]}" 71 | for kv in re.findall(_REGEX_CATCHALL, parsed) 72 | ] 73 | 74 | 75 | def parse_args(raw_prompt: str, config: Any) -> ParsedPrompt: 76 | arg_map = {} # Map from names to values 77 | warnings = [] 78 | 79 | def capture_value(arg: T, match): 80 | value, arg_warnings = arg.parse(match.group(1)) 81 | warnings.extend(arg_warnings) 82 | arg_map[arg.name] = value 83 | return "" 84 | 85 | # pylint: disable=protected-access 86 | for arg in config._config: 87 | raw_prompt = re.sub(_regex(arg.name), functools.partial(capture_value, arg), raw_prompt) 88 | 89 | unrecognized = _leftover_args(raw_prompt) 90 | if unrecognized: 91 | known_args = ", ".join(f"{arg.name} ({arg.get_type().__name__})" for arg in config._config) 92 | raise ValueError(f"Unrecognized arguments: {unrecognized}.\nKnown: {known_args}") 93 | 94 | result = config._result_type(**arg_map) 95 | 96 | return ParsedPrompt(cleaned=raw_prompt, result=result, warnings=warnings) 97 | -------------------------------------------------------------------------------- /comfyui_utils/test_gen_prompts.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | 4 | from comfyui_utils import gen_prompts 5 | 6 | def test_runs_at_all(): 7 | assert gen_prompts.parse_args("wow", gen_prompts.make_config("TestConfig", [])) 8 | 9 | def test_single(): 10 | config = gen_prompts.make_config("TestConfig", [ 11 | gen_prompts.IntArg("arg", 2, min_value=0, max_value=4) 12 | ]) 13 | 14 | # missing 15 | parsed = gen_prompts.parse_args("wow", config) 16 | assert not parsed.warnings 17 | assert parsed.cleaned == "wow" 18 | assert parsed.result.arg == 2 19 | 20 | # present 21 | parsed = gen_prompts.parse_args("wow $arg=3", config) 22 | assert len(parsed.warnings) == 0 23 | assert parsed.cleaned == "wow " 24 | assert parsed.result.arg == 3 25 | 26 | # int too large 27 | parsed = gen_prompts.parse_args("wow $arg=5", config) 28 | assert parsed.warnings == ["arg 5 too high, defaulting to 4"] 29 | assert parsed.cleaned == "wow " 30 | assert parsed.result.arg == 4 31 | 32 | # str doesn't parse 33 | config = gen_prompts.make_config("TestConfig", [ 34 | gen_prompts.IntArg("veg", 2, min_value=0, max_value=4) 35 | ]) 36 | with pytest.raises(Exception, match="Invalid argument veg: must be integer"): 37 | parsed = gen_prompts.parse_args("wow $veg=tomato", config) 38 | 39 | # wrong argument 40 | with pytest.raises(Exception, match=re.escape("Unrecognized arguments: ['wew=tomato']")): 41 | parsed = gen_prompts.parse_args("wow $wew=tomato", config) 42 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreyryabtsev/comfyui-python-api/56c4364c12ddaa4512ee95c53f04db0b497b7a14/examples/__init__.py -------------------------------------------------------------------------------- /examples/e2e.py: -------------------------------------------------------------------------------- 1 | """End-to-end example for creating an image with SDXL base + refiner""" 2 | 3 | import argparse 4 | import io 5 | import asyncio 6 | import json 7 | from comfyui_utils import comfy 8 | from comfyui_utils import gen_prompts 9 | 10 | async def run_base_and_refiner(address: str, user_string: str, output_path=None): 11 | comfyui = comfy.ComfyAPI(address) 12 | 13 | # Load the stored API format prompt. 14 | with open("examples/workflows/sdxl.json", "r", encoding="utf-8") as f: 15 | PROMPT_TEMPLATE = f.read() 16 | prompt = json.loads(PROMPT_TEMPLATE) 17 | # Config for parsing user arguments: 18 | prompt_config = gen_prompts.make_config("GenImg", [ 19 | gen_prompts.IntArg("base_steps", default_value=14, min_value=1, max_value=80), 20 | gen_prompts.IntArg("refiner_steps", default_value=5, min_value=0, max_value=40), 21 | gen_prompts.IntArg("seed", default_value=0, min_value=0, max_value=1e10) 22 | ]) 23 | # Extract the arguments from the user string. 24 | # Throws ValueError if arguments are unrecognized or malformed. 25 | # Returns a list of warnings if any argument is invalid. 26 | parsed = gen_prompts.parse_args(user_string, prompt_config) 27 | for warning in parsed.warnings: 28 | print(f"WARNING: {warning}") 29 | # Adjust the prompt with user args. 30 | prompt["6"]["inputs"]["text"] = parsed.cleaned 31 | prompt["10"]["inputs"]["end_at_step"] = parsed.result.base_steps 32 | prompt["11"]["inputs"]["start_at_step"] = parsed.result.base_steps 33 | prompt["10"]["inputs"]["steps"] = parsed.result.base_steps + parsed.result.refiner_steps 34 | prompt["10"]["inputs"]["noise_seed"] = parsed.result.seed 35 | prompt["11"]["inputs"]["steps"] = parsed.result.base_steps + parsed.result.refiner_steps 36 | 37 | # Prepare result dictionary. 38 | result = { 39 | "output": {}, 40 | "cached": False 41 | } 42 | # Configure the callbacks which will write to it during execution while printing updates. 43 | class Callbacks(comfy.Callbacks): 44 | async def queue_position(self, position): 45 | print(f"Queue position: #{position}") 46 | async def in_progress(self, node_id, progress, total): 47 | progress = f"{progress}/{total}" if total else None 48 | if node_id == 10: 49 | print(f"Base: {progress}" if progress else "Base...") 50 | if node_id == 11: 51 | print(f"Refiner: {progress}" if progress else "Refiner...") 52 | elif node_id == 17: 53 | print("Decoding...") 54 | elif node_id == 19: 55 | print("Saving image on backend...") 56 | async def completed(self, outputs, cached): 57 | result["output"] = outputs 58 | result["cached"] = cached 59 | 60 | # Run the prompt and print the result. 61 | print("Queuing workflow.") 62 | await comfyui.submit(prompt, Callbacks()) 63 | print(f"Result (cached: {'yes' if result['cached'] else 'no'}):\n{result['output']}") 64 | 65 | # Write the result to a local file. 66 | if output_path is not None: 67 | backend_filepath = result["output"]["images"][0] 68 | async def on_load(data_file : io.BytesIO): 69 | with open(output_path, "wb") as f: 70 | f.write(data_file.getbuffer()) 71 | await comfyui.fetch(backend_filepath, on_load) 72 | 73 | 74 | def main(): 75 | parser = argparse.ArgumentParser(description='Run an SDXL or other workflow on a deployed ComfyUI server.') 76 | parser.add_argument("--address", type=str, help="the ComfyUI endpoint", required=True) 77 | parser.add_argument("--prompt", type=str, help="the user prompt", required=True) 78 | parser.add_argument("--output_path", type=str, help="the output path", default=None) 79 | args = parser.parse_args() 80 | 81 | asyncio.run(run_base_and_refiner(args.address, args.prompt, args.output_path)) 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /examples/workflows/sdxl.json: -------------------------------------------------------------------------------- 1 | { 2 | "4": { 3 | "inputs": { 4 | "ckpt_name": "sd_xl_base_1.0.safetensors" 5 | }, 6 | "class_type": "CheckpointLoaderSimple" 7 | }, 8 | "5": { 9 | "inputs": { 10 | "width": 1152, 11 | "height": 832, 12 | "batch_size": 1 13 | }, 14 | "class_type": "EmptyLatentImage" 15 | }, 16 | "6": { 17 | "inputs": { 18 | "text": "spongebob squarepants in real life, detailed 4k close up with professional photography", 19 | "clip": [ 20 | "4", 21 | 1 22 | ] 23 | }, 24 | "class_type": "CLIPTextEncode" 25 | }, 26 | "7": { 27 | "inputs": { 28 | "text": "text, watermark", 29 | "clip": [ 30 | "4", 31 | 1 32 | ] 33 | }, 34 | "class_type": "CLIPTextEncode" 35 | }, 36 | "10": { 37 | "inputs": { 38 | "add_noise": "enable", 39 | "noise_seed": 591507825431037, 40 | "steps": 19, 41 | "cfg": 3, 42 | "sampler_name": "uni_pc", 43 | "scheduler": "normal", 44 | "start_at_step": 0, 45 | "end_at_step": 14, 46 | "return_with_leftover_noise": "enable", 47 | "model": [ 48 | "4", 49 | 0 50 | ], 51 | "positive": [ 52 | "6", 53 | 0 54 | ], 55 | "negative": [ 56 | "7", 57 | 0 58 | ], 59 | "latent_image": [ 60 | "5", 61 | 0 62 | ] 63 | }, 64 | "class_type": "KSamplerAdvanced" 65 | }, 66 | "11": { 67 | "inputs": { 68 | "add_noise": "disable", 69 | "noise_seed": 0, 70 | "steps": 19, 71 | "cfg": 3, 72 | "sampler_name": "uni_pc", 73 | "scheduler": "normal", 74 | "start_at_step": 14, 75 | "end_at_step": 10000, 76 | "return_with_leftover_noise": "disable", 77 | "model": [ 78 | "12", 79 | 0 80 | ], 81 | "positive": [ 82 | "15", 83 | 0 84 | ], 85 | "negative": [ 86 | "16", 87 | 0 88 | ], 89 | "latent_image": [ 90 | "10", 91 | 0 92 | ] 93 | }, 94 | "class_type": "KSamplerAdvanced" 95 | }, 96 | "12": { 97 | "inputs": { 98 | "ckpt_name": "sd_xl_refiner_1.0.safetensors" 99 | }, 100 | "class_type": "CheckpointLoaderSimple" 101 | }, 102 | "15": { 103 | "inputs": { 104 | "text": "potato", 105 | "clip": [ 106 | "12", 107 | 1 108 | ] 109 | }, 110 | "class_type": "CLIPTextEncode" 111 | }, 112 | "16": { 113 | "inputs": { 114 | "text": "text, watermark", 115 | "clip": [ 116 | "12", 117 | 1 118 | ] 119 | }, 120 | "class_type": "CLIPTextEncode" 121 | }, 122 | "17": { 123 | "inputs": { 124 | "samples": [ 125 | "11", 126 | 0 127 | ], 128 | "vae": [ 129 | "12", 130 | 2 131 | ] 132 | }, 133 | "class_type": "VAEDecode" 134 | }, 135 | "19": { 136 | "inputs": { 137 | "filename_prefix": "ComfyUI", 138 | "images": [ 139 | "17", 140 | 0 141 | ] 142 | }, 143 | "class_type": "SaveImage" 144 | } 145 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "comfyui_utils" 7 | version = "0.0.1" 8 | description = "Utilities for working with the ComfyUI API." 9 | readme = "README.md" 10 | authors = [{ name = "Andrey Ryabtsev", email = "ryabtsev.code@gmail.com" }] 11 | license = { file = "LICENSE" } 12 | classifiers = [ 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python", 15 | "Programming Language :: Python :: 3", 16 | ] 17 | keywords = ["comfyui", "api"] 18 | dependencies = [ 19 | "aiohttp" 20 | ] 21 | requires-python = ">=3.9" 22 | 23 | [project.urls] 24 | Homepage = "https://github.com/andreyryabtsev/comfyui_utils" 25 | 26 | [project.scripts] 27 | comfy_ui_example_e2e = "examples.e2e:main" 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='comfyui_utils', version='0.0', packages=find_packages()) 4 | --------------------------------------------------------------------------------