├── test_env ├── lib64 ├── bin │ ├── python │ ├── python3.12 │ ├── python3 │ ├── isort │ ├── nodeenv │ ├── flake8 │ ├── mypyc │ ├── pyflakes │ ├── stubgen │ ├── stubtest │ ├── black │ ├── coverage │ ├── coverage3 │ ├── identify-cli │ ├── pip │ ├── pip3 │ ├── pycodestyle │ ├── pygmentize │ ├── blackd │ ├── coverage-3.12 │ ├── pip3.12 │ ├── py.test │ ├── pytest │ ├── mypy │ ├── dmypy │ ├── normalizer │ ├── virtualenv │ ├── isort-identify-imports │ ├── activate.csh │ ├── activate │ ├── activate.fish │ └── Activate.ps1 └── pyvenv.cfg ├── test_venv ├── lib64 ├── bin │ ├── python │ ├── python3.12 │ ├── python3 │ ├── keyring │ ├── twine │ ├── rst2s5 │ ├── docutils │ ├── pip │ ├── pip3 │ ├── pip3.12 │ ├── pygmentize │ ├── rst2html │ ├── rst2man │ ├── rst2odt │ ├── rst2xml │ ├── markdown-it │ ├── rst2html4 │ ├── rst2html5 │ ├── rst2latex │ ├── rst2xetex │ ├── normalizer │ ├── pyproject-build │ ├── rst2pseudoxml │ ├── activate.csh │ ├── activate │ ├── activate.fish │ └── Activate.ps1 └── pyvenv.cfg ├── invalid.json ├── tests ├── __init__.py ├── test_conversion.py ├── test_enhanced_features.py ├── test_error_handling.py ├── test_workflow_loading.py └── test_comfyui_client.py ├── requirements.txt ├── MANIFEST.in ├── Result Image.png ├── Result Image_async.png ├── enhanced_test_Result Image.png ├── test_async_api_Result Image.png ├── test_sync_api_Result Image.png ├── test_async_workflow_Result Image.png ├── test_sync_workflow_Result Image.png ├── comfyuiclient ├── __init__.py └── client.py ├── requirements-test.txt ├── .flake8 ├── mypy.ini ├── requirements-dev.txt ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── publish.yml │ └── ci.yml └── pull_request_template.md ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── setup.py ├── docs └── index.md ├── CHANGELOG.md ├── verify_format_detection.py ├── workflow_api.json ├── workflow_converted.json ├── CONTRIBUTING.md ├── TEST_README.md ├── pyproject.toml ├── .gitignore ├── demo_usage.py ├── workflow.json ├── README.md └── README_ja.md /test_env/lib64: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /test_venv/lib64: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /invalid.json: -------------------------------------------------------------------------------- 1 | { invalid json -------------------------------------------------------------------------------- /test_env/bin/python: -------------------------------------------------------------------------------- 1 | python3 -------------------------------------------------------------------------------- /test_venv/bin/python: -------------------------------------------------------------------------------- 1 | python3 -------------------------------------------------------------------------------- /test_env/bin/python3.12: -------------------------------------------------------------------------------- 1 | python3 -------------------------------------------------------------------------------- /test_venv/bin/python3.12: -------------------------------------------------------------------------------- 1 | python3 -------------------------------------------------------------------------------- /test_env/bin/python3: -------------------------------------------------------------------------------- 1 | /usr/bin/python3 -------------------------------------------------------------------------------- /test_venv/bin/python3: -------------------------------------------------------------------------------- 1 | /usr/bin/python3 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Tests for ComfyUI Client -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | aiohttp 3 | pillow 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt -------------------------------------------------------------------------------- /Result Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/Result Image.png -------------------------------------------------------------------------------- /Result Image_async.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/Result Image_async.png -------------------------------------------------------------------------------- /enhanced_test_Result Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/enhanced_test_Result Image.png -------------------------------------------------------------------------------- /test_async_api_Result Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/test_async_api_Result Image.png -------------------------------------------------------------------------------- /test_sync_api_Result Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/test_sync_api_Result Image.png -------------------------------------------------------------------------------- /test_async_workflow_Result Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/test_async_workflow_Result Image.png -------------------------------------------------------------------------------- /test_sync_workflow_Result Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarkwork/Comfyui_api_client/HEAD/test_sync_workflow_Result Image.png -------------------------------------------------------------------------------- /test_env/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /usr/bin 2 | include-system-site-packages = false 3 | version = 3.12.3 4 | executable = /usr/bin/python3.12 5 | command = /usr/bin/python3 -m venv /mnt/f/ai/Comfyui_api_client/test_env 6 | -------------------------------------------------------------------------------- /test_venv/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /usr/bin 2 | include-system-site-packages = false 3 | version = 3.12.3 4 | executable = /usr/bin/python3.12 5 | command = /usr/bin/python3 -m venv /mnt/f/ai/Comfyui_api_client/test_venv 6 | -------------------------------------------------------------------------------- /comfyuiclient/__init__.py: -------------------------------------------------------------------------------- 1 | """ComfyUI Client - A Python client for ComfyUI API""" 2 | 3 | from .client import ComfyUIClient, ComfyUIClientAsync, convert_workflow_to_api 4 | 5 | __version__ = "0.1.0" 6 | __all__ = ["ComfyUIClient", "ComfyUIClientAsync", "convert_workflow_to_api"] 7 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # Test dependencies 2 | pytest>=6.0.0 3 | pytest-asyncio>=0.18.0 4 | pytest-cov>=3.0.0 5 | pytest-timeout>=2.1.0 6 | pytest-mock>=3.6.0 7 | 8 | # Coverage 9 | coverage[toml]>=6.3.0 10 | 11 | # Test utilities 12 | responses>=0.20.0 13 | aioresponses>=0.7.3 -------------------------------------------------------------------------------- /test_env/bin/isort: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from isort.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/nodeenv: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from nodeenv import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/flake8: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from flake8.main.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/mypyc: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from mypyc.__main__ import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pyflakes: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pyflakes.api import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/stubgen: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from mypy.stubgen import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/stubtest: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from mypy.stubtest import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/keyring: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from keyring.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/twine: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from twine.__main__ import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/black: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from black import patched_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(patched_main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/coverage: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from coverage.cmdline import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/coverage3: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from coverage.cmdline import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/identify-cli: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from identify.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pip: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pip3: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pycodestyle: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pycodestyle import _main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(_main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pygmentize: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pygments.cmdline import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2s5: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2s5 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2s5()) 9 | -------------------------------------------------------------------------------- /test_env/bin/blackd: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from blackd import patched_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(patched_main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/coverage-3.12: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from coverage.cmdline import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pip3.12: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/py.test: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pytest import console_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(console_main()) 9 | -------------------------------------------------------------------------------- /test_env/bin/pytest: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pytest import console_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(console_main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/docutils: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.__main__ import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/pip: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/pip3: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/pip3.12: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/pygmentize: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pygments.cmdline import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2html: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2html 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2html()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2man: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2man 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2man()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2odt: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2odt 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2odt()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2xml: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2xml 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2xml()) 9 | -------------------------------------------------------------------------------- /test_env/bin/mypy: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from mypy.__main__ import console_entry 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(console_entry()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/markdown-it: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from markdown_it.cli.parse import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2html4: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2html4 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2html4()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2html5: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2html5 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2html5()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2latex: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2latex 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2latex()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2xetex: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2xetex 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2xetex()) 9 | -------------------------------------------------------------------------------- /test_env/bin/dmypy: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from mypy.dmypy.client import console_entry 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(console_entry()) 9 | -------------------------------------------------------------------------------- /test_env/bin/normalizer: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from charset_normalizer import cli 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(cli.cli_detect()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/normalizer: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from charset_normalizer import cli 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(cli.cli_detect()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/pyproject-build: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from build.__main__ import entrypoint 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(entrypoint()) 9 | -------------------------------------------------------------------------------- /test_venv/bin/rst2pseudoxml: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from docutils.core import rst2pseudoxml 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(rst2pseudoxml()) 9 | -------------------------------------------------------------------------------- /test_env/bin/virtualenv: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from virtualenv.__main__ import run_with_catch 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(run_with_catch()) 9 | -------------------------------------------------------------------------------- /test_env/bin/isort-identify-imports: -------------------------------------------------------------------------------- 1 | #!/mnt/f/ai/Comfyui_api_client/test_env/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from isort.main import identify_imports_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(identify_imports_main()) 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, W503 4 | exclude = 5 | .git, 6 | __pycache__, 7 | build, 8 | dist, 9 | .eggs, 10 | *.egg, 11 | .venv, 12 | venv, 13 | test_venv, 14 | .tox, 15 | .mypy_cache, 16 | .pytest_cache, 17 | .coverage, 18 | htmlcov, 19 | docs/_build, 20 | .github 21 | per-file-ignores = 22 | __init__.py:F401 23 | max-complexity = 10 -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.7 3 | warn_return_any = True 4 | warn_unused_configs = True 5 | disallow_untyped_defs = False 6 | ignore_missing_imports = True 7 | follow_imports = normal 8 | show_error_codes = True 9 | strict_optional = True 10 | warn_redundant_casts = True 11 | warn_unused_ignores = True 12 | warn_no_return = True 13 | warn_unreachable = True 14 | no_implicit_reexport = True 15 | 16 | [mypy-tests.*] 17 | ignore_errors = True 18 | 19 | [mypy-setup] 20 | ignore_errors = True -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Development dependencies 2 | -r requirements.txt 3 | -r requirements-test.txt 4 | 5 | # Code formatting 6 | black>=22.0.0 7 | isort>=5.10.0 8 | 9 | # Linting 10 | flake8>=4.0.0 11 | pylint>=2.12.0 12 | 13 | # Type checking 14 | mypy>=0.940 15 | types-requests 16 | types-aiohttp 17 | 18 | # Pre-commit hooks 19 | pre-commit>=2.17.0 20 | 21 | # Documentation 22 | sphinx>=4.4.0 23 | sphinx-rtd-theme>=1.0.0 24 | sphinx-autodoc-typehints>=1.17.0 25 | 26 | # Build tools 27 | build>=0.7.0 28 | twine>=3.8.0 29 | wheel>=0.37.0 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | 13 | # Python files 14 | [*.py] 15 | indent_style = space 16 | indent_size = 4 17 | max_line_length = 88 18 | 19 | # YAML files 20 | [*.{yml,yaml}] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | # JSON files 25 | [*.json] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | # Markdown files 30 | [*.md] 31 | trim_trailing_whitespace = false 32 | 33 | # Makefile 34 | [Makefile] 35 | indent_style = tab -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.9' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install build 26 | 27 | - name: Build package 28 | run: python -m build 29 | 30 | - name: Publish package 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Code Example** 24 | If applicable, add a minimal code example to help explain your problem. 25 | 26 | ```python 27 | # Your code here 28 | ``` 29 | 30 | **Environment:** 31 | - OS: [e.g. Windows, macOS, Linux] 32 | - Python version: [e.g. 3.8] 33 | - ComfyUI Client version: [e.g. 0.1.0] 34 | - ComfyUI version: [e.g. latest] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | - id: check-merge-conflict 10 | - id: debug-statements 11 | 12 | - repo: https://github.com/psf/black 13 | rev: 23.1.0 14 | hooks: 15 | - id: black 16 | language_version: python3 17 | 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.12.0 20 | hooks: 21 | - id: isort 22 | args: ["--profile", "black"] 23 | 24 | - repo: https://github.com/pycqa/flake8 25 | rev: 6.0.0 26 | hooks: 27 | - id: flake8 28 | additional_dependencies: [flake8-docstrings] 29 | 30 | - repo: https://github.com/pre-commit/mirrors-mypy 31 | rev: v1.0.1 32 | hooks: 33 | - id: mypy 34 | additional_dependencies: [types-requests, types-aiohttp] -------------------------------------------------------------------------------- /test_env/bin/activate.csh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.csh" *from csh*. 2 | # You cannot run it directly. 3 | 4 | # Created by Davide Di Blasi . 5 | # Ported to Python 3.3 venv by Andrew Svetlov 6 | 7 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' 8 | 9 | # Unset irrelevant variables. 10 | deactivate nondestructive 11 | 12 | setenv VIRTUAL_ENV /mnt/f/ai/Comfyui_api_client/test_env 13 | 14 | set _OLD_VIRTUAL_PATH="$PATH" 15 | setenv PATH "$VIRTUAL_ENV/"bin":$PATH" 16 | 17 | 18 | set _OLD_VIRTUAL_PROMPT="$prompt" 19 | 20 | if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then 21 | set prompt = '(test_env) '"$prompt" 22 | setenv VIRTUAL_ENV_PROMPT '(test_env) ' 23 | endif 24 | 25 | alias pydoc python -m pydoc 26 | 27 | rehash 28 | -------------------------------------------------------------------------------- /test_venv/bin/activate.csh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.csh" *from csh*. 2 | # You cannot run it directly. 3 | 4 | # Created by Davide Di Blasi . 5 | # Ported to Python 3.3 venv by Andrew Svetlov 6 | 7 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' 8 | 9 | # Unset irrelevant variables. 10 | deactivate nondestructive 11 | 12 | setenv VIRTUAL_ENV /mnt/f/ai/Comfyui_api_client/test_venv 13 | 14 | set _OLD_VIRTUAL_PATH="$PATH" 15 | setenv PATH "$VIRTUAL_ENV/"bin":$PATH" 16 | 17 | 18 | set _OLD_VIRTUAL_PROMPT="$prompt" 19 | 20 | if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then 21 | set prompt = '(test_venv) '"$prompt" 22 | setenv VIRTUAL_ENV_PROMPT '(test_venv) ' 23 | endif 24 | 25 | alias pydoc python -m pydoc 26 | 27 | rehash 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sugarkwork 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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | ## Checklist: 24 | 25 | - [ ] My code follows the style guidelines of this project 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] I have added tests that prove my fix is effective or that my feature works 31 | - [ ] New and existing unit tests pass locally with my changes -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help install install-dev test lint format clean build publish 2 | 3 | help: 4 | @echo "Available commands:" 5 | @echo " install Install package" 6 | @echo " install-dev Install development dependencies" 7 | @echo " test Run tests" 8 | @echo " lint Run linting" 9 | @echo " format Format code" 10 | @echo " clean Clean build artifacts" 11 | @echo " build Build package" 12 | @echo " publish Publish to PyPI" 13 | 14 | install: 15 | pip install -e . 16 | 17 | install-dev: 18 | pip install -e ".[dev]" 19 | 20 | test: 21 | pytest tests/ -v 22 | 23 | test-cov: 24 | pytest tests/ -v --cov=comfyuiclient --cov-report=term-missing 25 | 26 | lint: 27 | flake8 comfyuiclient tests 28 | mypy comfyuiclient 29 | black --check comfyuiclient tests 30 | isort --check-only comfyuiclient tests 31 | 32 | format: 33 | black comfyuiclient tests 34 | isort comfyuiclient tests 35 | 36 | clean: 37 | rm -rf build/ 38 | rm -rf dist/ 39 | rm -rf *.egg-info/ 40 | rm -rf .pytest_cache/ 41 | rm -rf .coverage 42 | rm -rf htmlcov/ 43 | find . -type d -name __pycache__ -exec rm -rf {} + 44 | find . -type f -name "*.pyc" -delete 45 | 46 | build: clean 47 | python -m build 48 | 49 | publish: build 50 | python -m twine upload dist/* -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="comfyui-workflow-client", 8 | version="0.1.0", 9 | author="sugarkwork", 10 | description="A Python client for ComfyUI API", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/sugarkwork/Comfyui_api_client", 14 | packages=find_packages(), 15 | classifiers=[ 16 | "Development Status :: 3 - Alpha", 17 | "Intended Audience :: Developers", 18 | "Topic :: Software Development :: Libraries :: Python Modules", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | ], 27 | python_requires=">=3.7", 28 | install_requires=[ 29 | "requests", 30 | "aiohttp", 31 | "pillow", 32 | ], 33 | keywords="comfyui api client stable-diffusion", 34 | project_urls={ 35 | "Bug Reports": "https://github.com/sugarkwork/Comfyui_api_client/issues", 36 | "Source": "https://github.com/sugarkwork/Comfyui_api_client", 37 | }, 38 | ) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Client Documentation 2 | 3 | Welcome to the ComfyUI Client documentation. This library provides a Python interface for interacting with ComfyUI via its API. 4 | 5 | ## Quick Links 6 | 7 | - [Installation](installation.md) 8 | - [Quick Start Guide](quickstart.md) 9 | - [API Reference](api.md) 10 | - [Examples](examples.md) 11 | - [Contributing](contributing.md) 12 | 13 | ## Features 14 | 15 | - 🔄 **Dual Client Support**: Both sync and async implementations 16 | - 🎯 **Automatic Format Detection**: Automatically converts workflows 17 | - 🛠️ **Enhanced Configuration**: Flexible parameter setting 18 | - 🐛 **Debug Mode**: Development and troubleshooting support 19 | - 🔧 **Dynamic Reload**: Reload workflows without restarting 20 | - 🛡️ **Robust Error Handling**: Comprehensive error messages 21 | - 🔍 **Smart Node Lookup**: Find nodes by title or class_type 22 | - 📦 **Image Upload Support**: Direct image upload to ComfyUI 23 | 24 | ## Getting Started 25 | 26 | ```python 27 | from comfyuiclient import ComfyUIClient 28 | 29 | # Initialize client 30 | client = ComfyUIClient("localhost:8188", "workflow.json") 31 | client.connect() 32 | 33 | # Set parameters 34 | client.set_data(key='KSampler', seed=12345) 35 | client.set_data(key='CLIP Text Encode Positive', text="beautiful landscape") 36 | 37 | # Generate images 38 | results = client.generate(["Result Image"]) 39 | for key, image in results.items(): 40 | image.save(f"{key}.png") 41 | 42 | client.close() 43 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Modern Python packaging with pyproject.toml 12 | - GitHub Actions CI/CD workflows 13 | - Comprehensive test suite organization 14 | - Development and test requirements files 15 | - Code quality tool configurations (flake8, mypy, black) 16 | - Documentation structure 17 | - Contributing guidelines 18 | - Code of Conduct 19 | - Makefile for development automation 20 | - Pre-commit configuration 21 | 22 | ## [0.1.0] - 2025-01-06 23 | 24 | ### Added 25 | - Initial release of ComfyUI Client 26 | - Synchronous client (`ComfyUIClient`) 27 | - Asynchronous client (`ComfyUIClientAsync`) 28 | - Automatic workflow format conversion 29 | - Enhanced `set_data()` method for all parameter types 30 | - Debug mode for development and troubleshooting 31 | - Dynamic workflow reloading 32 | - Comprehensive error handling 33 | - Smart node lookup by title or class_type 34 | - Direct image upload support 35 | - Timeout handling for long-running operations 36 | - Resource cleanup on connection close 37 | 38 | ### Features 39 | - Support for both workflow.json and workflow_api.json formats 40 | - Flexible parameter setting with various input types 41 | - Image generation from specified nodes 42 | - WebSocket-based communication with ComfyUI server 43 | - Automatic retry mechanism for failed operations 44 | 45 | [Unreleased]: https://github.com/sugarkwork/Comfyui_api_client/compare/v0.1.0...HEAD 46 | [0.1.0]: https://github.com/sugarkwork/Comfyui_api_client/releases/tag/v0.1.0 -------------------------------------------------------------------------------- /tests/test_conversion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Test conversion from workflow.json to API format""" 3 | 4 | import json 5 | from comfyuiclient import convert_workflow_to_api 6 | 7 | # Test conversion 8 | print("Testing workflow.json to API format conversion...") 9 | 10 | # Convert workflow.json 11 | api_format = convert_workflow_to_api('workflow.json') 12 | 13 | # Save converted format 14 | with open('workflow_converted.json', 'w', encoding='utf8') as f: 15 | json.dump(api_format, f, indent=2, ensure_ascii=False) 16 | 17 | print("Converted workflow saved to workflow_converted.json") 18 | 19 | # Compare with original workflow_api.json 20 | with open('workflow_api.json', 'r', encoding='utf8') as f: 21 | original_api = json.load(f) 22 | 23 | print("\nComparison with original workflow_api.json:") 24 | print(f"Original nodes: {list(original_api.keys())}") 25 | print(f"Converted nodes: {list(api_format.keys())}") 26 | 27 | # Check each node 28 | for node_id in original_api: 29 | if node_id in api_format: 30 | print(f"\nNode {node_id} ({original_api[node_id]['class_type']}):") 31 | orig_inputs = original_api[node_id]['inputs'] 32 | conv_inputs = api_format[node_id]['inputs'] 33 | 34 | # Compare inputs 35 | for key in orig_inputs: 36 | if key in conv_inputs: 37 | if orig_inputs[key] == conv_inputs[key]: 38 | print(f" ✓ {key}: {orig_inputs[key]}") 39 | else: 40 | print(f" ✗ {key}: {orig_inputs[key]} → {conv_inputs[key]}") 41 | else: 42 | print(f" - {key}: missing in converted") 43 | 44 | # Check for extra inputs in converted 45 | for key in conv_inputs: 46 | if key not in orig_inputs: 47 | print(f" + {key}: {conv_inputs[key]} (extra in converted)") 48 | 49 | print("\nConversion test complete!") -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -e ".[test]" 28 | 29 | - name: Run tests 30 | run: | 31 | pytest tests/ -v --cov=comfyuiclient --cov-report=xml 32 | 33 | - name: Upload coverage to Codecov 34 | if: matrix.python-version == '3.9' 35 | uses: codecov/codecov-action@v3 36 | with: 37 | file: ./coverage.xml 38 | fail_ci_if_error: true 39 | 40 | lint: 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: Set up Python 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: '3.9' 50 | 51 | - name: Install dependencies 52 | run: | 53 | python -m pip install --upgrade pip 54 | pip install -e ".[dev]" 55 | 56 | - name: Lint with flake8 57 | run: | 58 | flake8 comfyuiclient tests --count --select=E9,F63,F7,F82 --show-source --statistics 59 | flake8 comfyuiclient tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 60 | 61 | - name: Check with black 62 | run: | 63 | black --check comfyuiclient tests 64 | 65 | - name: Check imports with isort 66 | run: | 67 | isort --check-only comfyuiclient tests 68 | 69 | - name: Type check with mypy 70 | run: | 71 | mypy comfyuiclient -------------------------------------------------------------------------------- /verify_format_detection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Verify that format detection works correctly""" 3 | 4 | import json 5 | from comfyuiclient import ComfyUIClient 6 | 7 | def check_format_detection(): 8 | """Check if the format detection logic works correctly""" 9 | print("=== Verifying Format Detection ===\n") 10 | 11 | # Test workflow.json 12 | print("1. Loading workflow.json:") 13 | with open('workflow.json', 'r') as f: 14 | workflow_data = json.load(f) 15 | 16 | print(f" - Has 'nodes' key: {'nodes' in workflow_data}") 17 | print(f" - Has 'links' key: {'links' in workflow_data}") 18 | print(f" - Number of nodes: {len(workflow_data.get('nodes', []))}") 19 | print(f" - Should convert: YES\n") 20 | 21 | # Test workflow_api.json 22 | print("2. Loading workflow_api.json:") 23 | with open('workflow_api.json', 'r') as f: 24 | api_data = json.load(f) 25 | 26 | print(f" - Has 'nodes' key: {'nodes' in api_data}") 27 | print(f" - Has 'links' key: {'links' in api_data}") 28 | print(f" - Root keys are node IDs: {all(key.isdigit() for key in api_data.keys())}") 29 | print(f" - Should convert: NO\n") 30 | 31 | # Test actual client behavior 32 | print("3. Testing client auto-detection:") 33 | 34 | # Test with workflow.json 35 | client1 = ComfyUIClient("dummy:8188", "workflow.json") 36 | print(f" - workflow.json loaded, has node '3': {'3' in client1.comfyui_prompt}") 37 | print(f" - Node '3' class_type: {client1.comfyui_prompt.get('3', {}).get('class_type', 'N/A')}") 38 | 39 | # Test with workflow_api.json 40 | client2 = ComfyUIClient("dummy:8188", "workflow_api.json") 41 | print(f" - workflow_api.json loaded, has node '3': {'3' in client2.comfyui_prompt}") 42 | print(f" - Node '3' class_type: {client2.comfyui_prompt.get('3', {}).get('class_type', 'N/A')}") 43 | 44 | print("\n✓ Format detection is working correctly!") 45 | 46 | if __name__ == "__main__": 47 | check_format_detection() -------------------------------------------------------------------------------- /tests/test_enhanced_features.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Test enhanced features from other repositories""" 3 | 4 | import random 5 | import sys 6 | from comfyuiclient import ComfyUIClient 7 | 8 | def test_enhanced_features(): 9 | """Test the new enhanced features""" 10 | print("=== Testing Enhanced ComfyUI Client Features ===\n") 11 | 12 | # Test with debug mode 13 | print("1. Testing with debug mode enabled:") 14 | client = ComfyUIClient("192.168.1.27:8188", "workflow.json", debug=True) 15 | client.connect() 16 | 17 | # Test enhanced set_data with arbitrary input 18 | print("\n2. Testing enhanced set_data with arbitrary input:") 19 | client.set_data(key='KSampler', input_key='steps', input_value=15) 20 | 21 | # Test number and value parameters 22 | print("\n3. Testing number and value parameters:") 23 | client.set_data(key='EmptyLatentImage', number=256.0) 24 | 25 | # Test reload functionality 26 | print("\n4. Testing reload functionality:") 27 | client.reload() 28 | 29 | # Test class_type lookup 30 | print("\n5. Testing class_type lookup:") 31 | ksampler_id = client.find_key_by_title('KSampler') 32 | print(f"Found KSampler by class_type: {ksampler_id}") 33 | 34 | # Generate image with enhanced features 35 | print("\n6. Generating image with enhanced features:") 36 | client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 37 | client.set_data(key='CLIP Text Encode Positive', text="enhanced magical forest with glowing mushrooms") 38 | 39 | try: 40 | results = client.generate(["Result Image"]) 41 | for key, image in results.items(): 42 | filename = f"enhanced_test_{key}.png" 43 | image.save(filename) 44 | print(f"✓ Success! Saved: {filename}") 45 | except Exception as e: 46 | print(f"✗ Error: {e}") 47 | finally: 48 | client.close() 49 | 50 | print("\n✓ Enhanced features test completed!") 51 | 52 | if __name__ == "__main__": 53 | test_enhanced_features() -------------------------------------------------------------------------------- /workflow_api.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "inputs": { 4 | "seed": 694907290331113, 5 | "steps": 20, 6 | "cfg": 8, 7 | "sampler_name": "euler", 8 | "scheduler": "normal", 9 | "denoise": 1, 10 | "model": [ 11 | "4", 12 | 0 13 | ], 14 | "positive": [ 15 | "6", 16 | 0 17 | ], 18 | "negative": [ 19 | "7", 20 | 0 21 | ], 22 | "latent_image": [ 23 | "5", 24 | 0 25 | ] 26 | }, 27 | "class_type": "KSampler", 28 | "_meta": { 29 | "title": "KSampler" 30 | } 31 | }, 32 | "4": { 33 | "inputs": { 34 | "ckpt_name": "v1-5-pruned.safetensors" 35 | }, 36 | "class_type": "CheckpointLoaderSimple", 37 | "_meta": { 38 | "title": "チェックポイントを読み込む" 39 | } 40 | }, 41 | "5": { 42 | "inputs": { 43 | "width": 512, 44 | "height": 512, 45 | "batch_size": 1 46 | }, 47 | "class_type": "EmptyLatentImage", 48 | "_meta": { 49 | "title": "空の潜在画像" 50 | } 51 | }, 52 | "6": { 53 | "inputs": { 54 | "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", 55 | "clip": [ 56 | "4", 57 | 1 58 | ] 59 | }, 60 | "class_type": "CLIPTextEncode", 61 | "_meta": { 62 | "title": "CLIP Text Encode Positive" 63 | } 64 | }, 65 | "7": { 66 | "inputs": { 67 | "text": "text, watermark", 68 | "clip": [ 69 | "4", 70 | 1 71 | ] 72 | }, 73 | "class_type": "CLIPTextEncode", 74 | "_meta": { 75 | "title": "CLIP Text Encode Negative" 76 | } 77 | }, 78 | "8": { 79 | "inputs": { 80 | "samples": [ 81 | "3", 82 | 0 83 | ], 84 | "vae": [ 85 | "4", 86 | 2 87 | ] 88 | }, 89 | "class_type": "VAEDecode", 90 | "_meta": { 91 | "title": "VAEデコード" 92 | } 93 | }, 94 | "10": { 95 | "inputs": { 96 | "images": [ 97 | "8", 98 | 0 99 | ] 100 | }, 101 | "class_type": "PreviewImage", 102 | "_meta": { 103 | "title": "Result Image" 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /workflow_converted.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "class_type": "KSampler", 4 | "_meta": { 5 | "title": "KSampler" 6 | }, 7 | "inputs": { 8 | "seed": 694907290331113, 9 | "steps": 20, 10 | "cfg": 8, 11 | "sampler_name": "euler", 12 | "scheduler": "normal", 13 | "denoise": 1, 14 | "model": [ 15 | "4", 16 | 0 17 | ], 18 | "positive": [ 19 | "6", 20 | 0 21 | ], 22 | "negative": [ 23 | "7", 24 | 0 25 | ], 26 | "latent_image": [ 27 | "5", 28 | 0 29 | ] 30 | } 31 | }, 32 | "8": { 33 | "class_type": "VAEDecode", 34 | "_meta": { 35 | "title": "VAEDecode" 36 | }, 37 | "inputs": { 38 | "samples": [ 39 | "3", 40 | 0 41 | ], 42 | "vae": [ 43 | "4", 44 | 2 45 | ] 46 | } 47 | }, 48 | "10": { 49 | "class_type": "PreviewImage", 50 | "_meta": { 51 | "title": "Result Image" 52 | }, 53 | "inputs": { 54 | "images": [ 55 | "8", 56 | 0 57 | ] 58 | } 59 | }, 60 | "6": { 61 | "class_type": "CLIPTextEncode", 62 | "_meta": { 63 | "title": "CLIP Text Encode Positive" 64 | }, 65 | "inputs": { 66 | "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", 67 | "clip": [ 68 | "4", 69 | 1 70 | ] 71 | } 72 | }, 73 | "7": { 74 | "class_type": "CLIPTextEncode", 75 | "_meta": { 76 | "title": "CLIP Text Encode Negative" 77 | }, 78 | "inputs": { 79 | "text": "text, watermark", 80 | "clip": [ 81 | "4", 82 | 1 83 | ] 84 | } 85 | }, 86 | "5": { 87 | "class_type": "EmptyLatentImage", 88 | "_meta": { 89 | "title": "EmptyLatentImage" 90 | }, 91 | "inputs": { 92 | "width": 512, 93 | "height": 512, 94 | "batch_size": 1 95 | } 96 | }, 97 | "4": { 98 | "class_type": "CheckpointLoaderSimple", 99 | "_meta": { 100 | "title": "CheckpointLoaderSimple" 101 | }, 102 | "inputs": { 103 | "ckpt_name": "illusionbreed_v20.safetensors" 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /test_env/bin/activate: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # You cannot run it directly 3 | 4 | deactivate () { 5 | # reset old environment variables 6 | if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then 7 | PATH="${_OLD_VIRTUAL_PATH:-}" 8 | export PATH 9 | unset _OLD_VIRTUAL_PATH 10 | fi 11 | if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then 12 | PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" 13 | export PYTHONHOME 14 | unset _OLD_VIRTUAL_PYTHONHOME 15 | fi 16 | 17 | # Call hash to forget past commands. Without forgetting 18 | # past commands the $PATH changes we made may not be respected 19 | hash -r 2> /dev/null 20 | 21 | if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then 22 | PS1="${_OLD_VIRTUAL_PS1:-}" 23 | export PS1 24 | unset _OLD_VIRTUAL_PS1 25 | fi 26 | 27 | unset VIRTUAL_ENV 28 | unset VIRTUAL_ENV_PROMPT 29 | if [ ! "${1:-}" = "nondestructive" ] ; then 30 | # Self destruct! 31 | unset -f deactivate 32 | fi 33 | } 34 | 35 | # unset irrelevant variables 36 | deactivate nondestructive 37 | 38 | # on Windows, a path can contain colons and backslashes and has to be converted: 39 | if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then 40 | # transform D:\path\to\venv to /d/path/to/venv on MSYS 41 | # and to /cygdrive/d/path/to/venv on Cygwin 42 | export VIRTUAL_ENV=$(cygpath /mnt/f/ai/Comfyui_api_client/test_env) 43 | else 44 | # use the path as-is 45 | export VIRTUAL_ENV=/mnt/f/ai/Comfyui_api_client/test_env 46 | fi 47 | 48 | _OLD_VIRTUAL_PATH="$PATH" 49 | PATH="$VIRTUAL_ENV/"bin":$PATH" 50 | export PATH 51 | 52 | # unset PYTHONHOME if set 53 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) 54 | # could use `if (set -u; : $PYTHONHOME) ;` in bash 55 | if [ -n "${PYTHONHOME:-}" ] ; then 56 | _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" 57 | unset PYTHONHOME 58 | fi 59 | 60 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then 61 | _OLD_VIRTUAL_PS1="${PS1:-}" 62 | PS1='(test_env) '"${PS1:-}" 63 | export PS1 64 | VIRTUAL_ENV_PROMPT='(test_env) ' 65 | export VIRTUAL_ENV_PROMPT 66 | fi 67 | 68 | # Call hash to forget past commands. Without forgetting 69 | # past commands the $PATH changes we made may not be respected 70 | hash -r 2> /dev/null 71 | -------------------------------------------------------------------------------- /test_venv/bin/activate: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # You cannot run it directly 3 | 4 | deactivate () { 5 | # reset old environment variables 6 | if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then 7 | PATH="${_OLD_VIRTUAL_PATH:-}" 8 | export PATH 9 | unset _OLD_VIRTUAL_PATH 10 | fi 11 | if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then 12 | PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" 13 | export PYTHONHOME 14 | unset _OLD_VIRTUAL_PYTHONHOME 15 | fi 16 | 17 | # Call hash to forget past commands. Without forgetting 18 | # past commands the $PATH changes we made may not be respected 19 | hash -r 2> /dev/null 20 | 21 | if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then 22 | PS1="${_OLD_VIRTUAL_PS1:-}" 23 | export PS1 24 | unset _OLD_VIRTUAL_PS1 25 | fi 26 | 27 | unset VIRTUAL_ENV 28 | unset VIRTUAL_ENV_PROMPT 29 | if [ ! "${1:-}" = "nondestructive" ] ; then 30 | # Self destruct! 31 | unset -f deactivate 32 | fi 33 | } 34 | 35 | # unset irrelevant variables 36 | deactivate nondestructive 37 | 38 | # on Windows, a path can contain colons and backslashes and has to be converted: 39 | if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then 40 | # transform D:\path\to\venv to /d/path/to/venv on MSYS 41 | # and to /cygdrive/d/path/to/venv on Cygwin 42 | export VIRTUAL_ENV=$(cygpath /mnt/f/ai/Comfyui_api_client/test_venv) 43 | else 44 | # use the path as-is 45 | export VIRTUAL_ENV=/mnt/f/ai/Comfyui_api_client/test_venv 46 | fi 47 | 48 | _OLD_VIRTUAL_PATH="$PATH" 49 | PATH="$VIRTUAL_ENV/"bin":$PATH" 50 | export PATH 51 | 52 | # unset PYTHONHOME if set 53 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) 54 | # could use `if (set -u; : $PYTHONHOME) ;` in bash 55 | if [ -n "${PYTHONHOME:-}" ] ; then 56 | _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" 57 | unset PYTHONHOME 58 | fi 59 | 60 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then 61 | _OLD_VIRTUAL_PS1="${PS1:-}" 62 | PS1='(test_venv) '"${PS1:-}" 63 | export PS1 64 | VIRTUAL_ENV_PROMPT='(test_venv) ' 65 | export VIRTUAL_ENV_PROMPT 66 | fi 67 | 68 | # Call hash to forget past commands. Without forgetting 69 | # past commands the $PATH changes we made may not be respected 70 | hash -r 2> /dev/null 71 | -------------------------------------------------------------------------------- /test_env/bin/activate.fish: -------------------------------------------------------------------------------- 1 | # This file must be used with "source /bin/activate.fish" *from fish* 2 | # (https://fishshell.com/). You cannot run it directly. 3 | 4 | function deactivate -d "Exit virtual environment and return to normal shell environment" 5 | # reset old environment variables 6 | if test -n "$_OLD_VIRTUAL_PATH" 7 | set -gx PATH $_OLD_VIRTUAL_PATH 8 | set -e _OLD_VIRTUAL_PATH 9 | end 10 | if test -n "$_OLD_VIRTUAL_PYTHONHOME" 11 | set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME 12 | set -e _OLD_VIRTUAL_PYTHONHOME 13 | end 14 | 15 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE" 16 | set -e _OLD_FISH_PROMPT_OVERRIDE 17 | # prevents error when using nested fish instances (Issue #93858) 18 | if functions -q _old_fish_prompt 19 | functions -e fish_prompt 20 | functions -c _old_fish_prompt fish_prompt 21 | functions -e _old_fish_prompt 22 | end 23 | end 24 | 25 | set -e VIRTUAL_ENV 26 | set -e VIRTUAL_ENV_PROMPT 27 | if test "$argv[1]" != "nondestructive" 28 | # Self-destruct! 29 | functions -e deactivate 30 | end 31 | end 32 | 33 | # Unset irrelevant variables. 34 | deactivate nondestructive 35 | 36 | set -gx VIRTUAL_ENV /mnt/f/ai/Comfyui_api_client/test_env 37 | 38 | set -gx _OLD_VIRTUAL_PATH $PATH 39 | set -gx PATH "$VIRTUAL_ENV/"bin $PATH 40 | 41 | # Unset PYTHONHOME if set. 42 | if set -q PYTHONHOME 43 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME 44 | set -e PYTHONHOME 45 | end 46 | 47 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" 48 | # fish uses a function instead of an env var to generate the prompt. 49 | 50 | # Save the current fish_prompt function as the function _old_fish_prompt. 51 | functions -c fish_prompt _old_fish_prompt 52 | 53 | # With the original prompt function renamed, we can override with our own. 54 | function fish_prompt 55 | # Save the return status of the last command. 56 | set -l old_status $status 57 | 58 | # Output the venv prompt; color taken from the blue of the Python logo. 59 | printf "%s%s%s" (set_color 4B8BBE) '(test_env) ' (set_color normal) 60 | 61 | # Restore the return status of the previous command. 62 | echo "exit $old_status" | . 63 | # Output the original/"old" prompt. 64 | _old_fish_prompt 65 | end 66 | 67 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" 68 | set -gx VIRTUAL_ENV_PROMPT '(test_env) ' 69 | end 70 | -------------------------------------------------------------------------------- /test_venv/bin/activate.fish: -------------------------------------------------------------------------------- 1 | # This file must be used with "source /bin/activate.fish" *from fish* 2 | # (https://fishshell.com/). You cannot run it directly. 3 | 4 | function deactivate -d "Exit virtual environment and return to normal shell environment" 5 | # reset old environment variables 6 | if test -n "$_OLD_VIRTUAL_PATH" 7 | set -gx PATH $_OLD_VIRTUAL_PATH 8 | set -e _OLD_VIRTUAL_PATH 9 | end 10 | if test -n "$_OLD_VIRTUAL_PYTHONHOME" 11 | set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME 12 | set -e _OLD_VIRTUAL_PYTHONHOME 13 | end 14 | 15 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE" 16 | set -e _OLD_FISH_PROMPT_OVERRIDE 17 | # prevents error when using nested fish instances (Issue #93858) 18 | if functions -q _old_fish_prompt 19 | functions -e fish_prompt 20 | functions -c _old_fish_prompt fish_prompt 21 | functions -e _old_fish_prompt 22 | end 23 | end 24 | 25 | set -e VIRTUAL_ENV 26 | set -e VIRTUAL_ENV_PROMPT 27 | if test "$argv[1]" != "nondestructive" 28 | # Self-destruct! 29 | functions -e deactivate 30 | end 31 | end 32 | 33 | # Unset irrelevant variables. 34 | deactivate nondestructive 35 | 36 | set -gx VIRTUAL_ENV /mnt/f/ai/Comfyui_api_client/test_venv 37 | 38 | set -gx _OLD_VIRTUAL_PATH $PATH 39 | set -gx PATH "$VIRTUAL_ENV/"bin $PATH 40 | 41 | # Unset PYTHONHOME if set. 42 | if set -q PYTHONHOME 43 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME 44 | set -e PYTHONHOME 45 | end 46 | 47 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" 48 | # fish uses a function instead of an env var to generate the prompt. 49 | 50 | # Save the current fish_prompt function as the function _old_fish_prompt. 51 | functions -c fish_prompt _old_fish_prompt 52 | 53 | # With the original prompt function renamed, we can override with our own. 54 | function fish_prompt 55 | # Save the return status of the last command. 56 | set -l old_status $status 57 | 58 | # Output the venv prompt; color taken from the blue of the Python logo. 59 | printf "%s%s%s" (set_color 4B8BBE) '(test_venv) ' (set_color normal) 60 | 61 | # Restore the return status of the previous command. 62 | echo "exit $old_status" | . 63 | # Output the original/"old" prompt. 64 | _old_fish_prompt 65 | end 66 | 67 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" 68 | set -gx VIRTUAL_ENV_PROMPT '(test_venv) ' 69 | end 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ComfyUI Client 2 | 3 | Thank you for your interest in contributing to ComfyUI Client! We welcome contributions from the community. 4 | 5 | ## How to Contribute 6 | 7 | ### Reporting Issues 8 | 9 | - Use the [GitHub issue tracker](https://github.com/sugarkwork/Comfyui_api_client/issues) 10 | - Check if the issue already exists before creating a new one 11 | - Provide detailed information about the issue: 12 | - Steps to reproduce 13 | - Expected behavior 14 | - Actual behavior 15 | - System information 16 | 17 | ### Submitting Pull Requests 18 | 19 | 1. Fork the repository 20 | 2. Create a new branch for your feature or fix: 21 | ```bash 22 | git checkout -b feature/your-feature-name 23 | ``` 24 | 3. Make your changes 25 | 4. Add tests for new functionality 26 | 5. Run the test suite: 27 | ```bash 28 | pytest tests/ 29 | ``` 30 | 6. Format your code: 31 | ```bash 32 | black comfyuiclient tests 33 | isort comfyuiclient tests 34 | ``` 35 | 7. Run linting: 36 | ```bash 37 | flake8 comfyuiclient tests 38 | mypy comfyuiclient 39 | ``` 40 | 8. Commit your changes with a descriptive message 41 | 9. Push to your fork and submit a pull request 42 | 43 | ### Development Setup 44 | 45 | 1. Clone the repository: 46 | ```bash 47 | git clone https://github.com/sugarkwork/Comfyui_api_client.git 48 | cd Comfyui_api_client 49 | ``` 50 | 51 | 2. Create a virtual environment: 52 | ```bash 53 | python -m venv venv 54 | source venv/bin/activate # On Windows: venv\Scripts\activate 55 | ``` 56 | 57 | 3. Install development dependencies: 58 | ```bash 59 | pip install -e ".[dev]" 60 | ``` 61 | 62 | 4. Install pre-commit hooks: 63 | ```bash 64 | pre-commit install 65 | ``` 66 | 67 | ### Code Style 68 | 69 | - Follow PEP 8 guidelines 70 | - Use Black for code formatting 71 | - Use isort for import sorting 72 | - Maximum line length is 88 characters 73 | - Write descriptive docstrings for all public functions and classes 74 | 75 | ### Testing 76 | 77 | - Write tests for all new functionality 78 | - Ensure all tests pass before submitting PR 79 | - Aim for high test coverage 80 | - Use pytest for testing 81 | 82 | ### Documentation 83 | 84 | - Update documentation for new features 85 | - Include docstrings in your code 86 | - Update README if necessary 87 | - Add examples for new functionality 88 | 89 | ## Code of Conduct 90 | 91 | Please note that this project follows a Code of Conduct. By participating, you are expected to uphold this code. 92 | 93 | ## Questions? 94 | 95 | If you have questions, please open an issue on GitHub or reach out to the maintainers. 96 | 97 | Thank you for contributing! -------------------------------------------------------------------------------- /TEST_README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Client Test Suite 2 | 3 | This directory contains comprehensive tests for the ComfyUIClient library, which tests both synchronous and asynchronous versions with different workflow formats. 4 | 5 | ## Test Files 6 | 7 | ### `test_comfyui_client.py` 8 | A comprehensive test script that validates: 9 | - Format conversion from workflow.json to API format 10 | - Automatic format detection 11 | - All 4 client/format combinations 12 | - Server connectivity 13 | - Image generation 14 | 15 | ### `demo_usage.py` 16 | A simpler demo script showing practical usage examples for all 4 combinations. 17 | 18 | ## Running the Tests 19 | 20 | ### Prerequisites 21 | 1. Ensure ComfyUI server is running 22 | 2. Update `SERVER_ADDRESS` in both test scripts to match your server 23 | 3. Install required dependencies: 24 | ```bash 25 | pip install -r requirements.txt 26 | ``` 27 | 28 | ### Run Comprehensive Tests 29 | ```bash 30 | python test_comfyui_client.py 31 | ``` 32 | 33 | This will: 34 | - Test format conversion functionality 35 | - Test sync client with workflow.json (auto-conversion) 36 | - Test sync client with workflow_api.json (direct load) 37 | - Test async client with workflow.json (auto-conversion) 38 | - Test async client with workflow_api.json (direct load) 39 | - Generate test images for each successful combination 40 | - Display a summary of all test results 41 | 42 | ### Run Demo Script 43 | ```bash 44 | python demo_usage.py 45 | ``` 46 | 47 | This will generate 4 demo images using different prompts for each combination. 48 | 49 | ## Test Output 50 | 51 | The test script provides color-coded output: 52 | - ✓ Green: Successful operations 53 | - ✗ Red: Failed operations 54 | - ℹ Blue: Information messages 55 | - ⚠ Yellow: Warnings 56 | 57 | ## Expected Behavior 58 | 59 | ### Format Detection 60 | - **workflow.json**: Contains `nodes` and `links` arrays, automatically converted to API format 61 | - **workflow_api.json**: Already in API format, loaded directly without conversion 62 | 63 | ### Generated Files 64 | After successful test runs, you should see: 65 | - `test_sync_workflow.png` 66 | - `test_sync_workflow_api.png` 67 | - `test_async_workflow.png` 68 | - `test_async_workflow_api.png` 69 | 70 | And from the demo script: 71 | - `demo_sync_workflow.png` 72 | - `demo_sync_api.png` 73 | - `demo_async_workflow.png` 74 | - `demo_async_api.png` 75 | 76 | ## Troubleshooting 77 | 78 | ### Server Connection Failed 79 | - Ensure ComfyUI is running 80 | - Check the SERVER_ADDRESS in the test scripts 81 | - Verify firewall settings 82 | 83 | ### Node Not Found Errors 84 | - The workflow files may use different node names 85 | - Check the node titles in your workflow files 86 | - Common variations: "CLIP Text Encode Positive", "CLIPTextEncode", "positive" 87 | 88 | ### Missing Dependencies 89 | ```bash 90 | pip install requests aiohttp pillow 91 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "comfyui-workflow-client" 7 | version = "0.1.0" 8 | description = "A Python client for ComfyUI API" 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | license = {text = "MIT"} 12 | authors = [ 13 | {name = "sugarkwork"}, 14 | ] 15 | keywords = ["comfyui", "api", "client", "stable-diffusion", "workflow"] 16 | classifiers = [ 17 | "Development Status :: 3 - Alpha", 18 | "Intended Audience :: Developers", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | "License :: OSI Approved :: MIT License", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | ] 29 | dependencies = [ 30 | "requests", 31 | "aiohttp", 32 | "pillow", 33 | ] 34 | 35 | [project.urls] 36 | "Homepage" = "https://github.com/sugarkwork/Comfyui_api_client" 37 | "Bug Reports" = "https://github.com/sugarkwork/Comfyui_api_client/issues" 38 | "Source" = "https://github.com/sugarkwork/Comfyui_api_client" 39 | 40 | [project.optional-dependencies] 41 | dev = [ 42 | "pytest>=6.0", 43 | "pytest-asyncio", 44 | "pytest-cov", 45 | "black", 46 | "flake8", 47 | "mypy", 48 | "isort", 49 | "pre-commit", 50 | ] 51 | test = [ 52 | "pytest>=6.0", 53 | "pytest-asyncio", 54 | "pytest-cov", 55 | ] 56 | 57 | [tool.setuptools.packages.find] 58 | where = ["."] 59 | include = ["comfyuiclient*"] 60 | exclude = ["tests*"] 61 | 62 | [tool.black] 63 | line-length = 88 64 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] 65 | include = '\.pyi?$' 66 | extend-exclude = ''' 67 | /( 68 | # directories 69 | \.eggs 70 | | \.git 71 | | \.hg 72 | | \.mypy_cache 73 | | \.tox 74 | | \.venv 75 | | _build 76 | | buck-out 77 | | build 78 | | dist 79 | )/ 80 | ''' 81 | 82 | [tool.isort] 83 | profile = "black" 84 | multi_line_output = 3 85 | include_trailing_comma = true 86 | force_grid_wrap = 0 87 | use_parentheses = true 88 | ensure_newline_before_comments = true 89 | line_length = 88 90 | 91 | [tool.mypy] 92 | python_version = "3.7" 93 | warn_return_any = true 94 | warn_unused_configs = true 95 | disallow_untyped_defs = false 96 | ignore_missing_imports = true 97 | 98 | [tool.pytest.ini_options] 99 | minversion = "6.0" 100 | addopts = "-ra -q --strict-markers" 101 | testpaths = [ 102 | "tests", 103 | ] 104 | python_files = "test_*.py" 105 | python_classes = "Test*" 106 | python_functions = "test_*" 107 | 108 | [tool.coverage.run] 109 | source = ["comfyuiclient"] 110 | omit = ["*/tests/*", "*/test_*.py"] 111 | 112 | [tool.coverage.report] 113 | exclude_lines = [ 114 | "pragma: no cover", 115 | "def __repr__", 116 | "raise AssertionError", 117 | "raise NotImplementedError", 118 | "if __name__ == .__main__.:", 119 | ] -------------------------------------------------------------------------------- /tests/test_error_handling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Test error handling improvements""" 3 | 4 | import asyncio 5 | from comfyuiclient import ComfyUIClient, ComfyUIClientAsync 6 | 7 | def test_error_handling(): 8 | """Test various error conditions""" 9 | print("=== Testing Error Handling ===\n") 10 | 11 | # Test with invalid server 12 | print("1. Testing connection to invalid server:") 13 | try: 14 | client = ComfyUIClient("invalid-server:8188", "workflow_api.json", debug=True) 15 | client.connect() 16 | client.queue_prompt({}) 17 | print(" ✗ Should have raised ConnectionError") 18 | except ConnectionError as e: 19 | print(f" ✓ Caught expected ConnectionError: {e}") 20 | except Exception as e: 21 | print(f" ⚠ Unexpected error: {e}") 22 | 23 | # Test with missing file 24 | print("\n2. Testing missing workflow file:") 25 | try: 26 | client = ComfyUIClient("localhost:8188", "nonexistent.json", debug=True) 27 | print(" ✗ Should have raised FileNotFoundError") 28 | except FileNotFoundError: 29 | print(" ✓ Caught expected FileNotFoundError") 30 | except Exception as e: 31 | print(f" ✓ Handled file error gracefully: {e}") 32 | 33 | # Test with invalid JSON 34 | print("\n3. Testing invalid JSON file:") 35 | with open("invalid.json", "w") as f: 36 | f.write("{ invalid json") 37 | 38 | try: 39 | client = ComfyUIClient("localhost:8188", "invalid.json", debug=True) 40 | print(" ✗ Should have raised JSON error") 41 | except Exception as e: 42 | print(f" ✓ Handled JSON error gracefully: {e}") 43 | 44 | # Test find_key_by_title with non-existent key 45 | print("\n4. Testing non-existent key lookup:") 46 | try: 47 | client = ComfyUIClient("localhost:8188", "workflow_api.json", debug=False) 48 | result = client.find_key_by_title("NonExistentNode") 49 | if result is None: 50 | print(" ✓ Returns None for non-existent key (no debug output)") 51 | else: 52 | print(f" ✗ Expected None, got: {result}") 53 | except Exception as e: 54 | print(f" ✗ Unexpected error: {e}") 55 | 56 | # Test with debug mode 57 | print("\n5. Testing non-existent key lookup with debug:") 58 | try: 59 | client = ComfyUIClient("localhost:8188", "workflow_api.json", debug=True) 60 | result = client.find_key_by_title("NonExistentNode") 61 | if result is None: 62 | print(" ✓ Returns None for non-existent key (with debug output)") 63 | else: 64 | print(f" ✗ Expected None, got: {result}") 65 | except Exception as e: 66 | print(f" ✗ Unexpected error: {e}") 67 | 68 | print("\n✓ Error handling tests completed!") 69 | 70 | async def test_async_error_handling(): 71 | """Test async error handling""" 72 | print("\n=== Testing Async Error Handling ===\n") 73 | 74 | # Test async connection to invalid server 75 | print("1. Testing async connection to invalid server:") 76 | client = None 77 | try: 78 | client = ComfyUIClientAsync("invalid-server:8188", "workflow_api.json", debug=True) 79 | await client.connect() 80 | print(" ✗ Should have raised ConnectionError") 81 | except ConnectionError as e: 82 | print(f" ✓ Caught expected ConnectionError: {e}") 83 | except Exception as e: 84 | print(f" ⚠ Unexpected error: {e}") 85 | finally: 86 | if client: 87 | try: 88 | await client.close() 89 | except: 90 | pass 91 | 92 | print("\n✓ Async error handling tests completed!") 93 | 94 | async def main(): 95 | test_error_handling() 96 | await test_async_error_handling() 97 | 98 | # Cleanup 99 | import os 100 | try: 101 | os.remove("invalid.json") 102 | except: 103 | pass 104 | 105 | if __name__ == "__main__": 106 | asyncio.run(main()) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # VSCode 163 | .vscode/ 164 | 165 | # Test images 166 | *.png 167 | !Result Image.png 168 | !Result Image_async.png 169 | !enhanced_test_Result Image.png 170 | !test_async_api_Result Image.png 171 | !test_async_workflow_Result Image.png 172 | !test_sync_api_Result Image.png 173 | !test_sync_workflow_Result Image.png 174 | -------------------------------------------------------------------------------- /demo_usage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Demo script showing practical usage of ComfyUIClient 4 | """ 5 | 6 | import random 7 | import sys 8 | import asyncio 9 | from comfyuiclient import ComfyUIClient, ComfyUIClientAsync 10 | 11 | # Configuration - Update this to your ComfyUI server 12 | SERVER_ADDRESS = "192.168.1.27:8188" 13 | 14 | 15 | def demo_sync_workflow_json(): 16 | """Demo using sync client with workflow.json (auto-conversion)""" 17 | print("\n--- Sync Client with workflow.json ---") 18 | 19 | client = ComfyUIClient(SERVER_ADDRESS, "workflow.json") 20 | client.connect() 21 | 22 | try: 23 | # Set a random seed 24 | client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 25 | 26 | # Set prompt 27 | client.set_data(key='CLIP Text Encode Positive', text="a majestic eagle soaring through clouds") 28 | 29 | # Generate and save 30 | results = client.generate(["Result Image"]) 31 | for key, image in results.items(): 32 | filename = "demo_sync_workflow.png" 33 | image.save(filename) 34 | print(f"Saved: {filename}") 35 | 36 | finally: 37 | client.close() 38 | 39 | 40 | def demo_sync_api_json(): 41 | """Demo using sync client with workflow_api.json (direct load)""" 42 | print("\n--- Sync Client with workflow_api.json ---") 43 | 44 | client = ComfyUIClient(SERVER_ADDRESS, "workflow_api.json") 45 | client.connect() 46 | 47 | try: 48 | # Set a random seed 49 | client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 50 | 51 | # Set prompt 52 | client.set_data(key='CLIP Text Encode Positive', text="a futuristic cityscape at night") 53 | 54 | # Generate and save 55 | results = client.generate(["Result Image"]) 56 | for key, image in results.items(): 57 | filename = "demo_sync_api.png" 58 | image.save(filename) 59 | print(f"Saved: {filename}") 60 | 61 | finally: 62 | client.close() 63 | 64 | 65 | async def demo_async_workflow_json(): 66 | """Demo using async client with workflow.json (auto-conversion)""" 67 | print("\n--- Async Client with workflow.json ---") 68 | 69 | client = ComfyUIClientAsync(SERVER_ADDRESS, "workflow.json") 70 | await client.connect() 71 | 72 | try: 73 | # Set a random seed 74 | await client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 75 | 76 | # Set prompt 77 | await client.set_data(key='CLIP Text Encode Positive', text="a serene Japanese garden in autumn") 78 | 79 | # Generate and save 80 | results = await client.generate(["Result Image"]) 81 | for key, image in results.items(): 82 | filename = "demo_async_workflow.png" 83 | image.save(filename) 84 | print(f"Saved: {filename}") 85 | 86 | finally: 87 | await client.close() 88 | 89 | 90 | async def demo_async_api_json(): 91 | """Demo using async client with workflow_api.json (direct load)""" 92 | print("\n--- Async Client with workflow_api.json ---") 93 | 94 | client = ComfyUIClientAsync(SERVER_ADDRESS, "workflow_api.json") 95 | await client.connect() 96 | 97 | try: 98 | # Set a random seed 99 | await client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 100 | 101 | # Set prompt 102 | await client.set_data(key='CLIP Text Encode Positive', text="a magical forest with glowing mushrooms") 103 | 104 | # Generate and save 105 | results = await client.generate(["Result Image"]) 106 | for key, image in results.items(): 107 | filename = "demo_async_api.png" 108 | image.save(filename) 109 | print(f"Saved: {filename}") 110 | 111 | finally: 112 | await client.close() 113 | 114 | 115 | async def main(): 116 | """Run all demos""" 117 | print("ComfyUI Client Demo - All 4 Combinations") 118 | print("=" * 50) 119 | 120 | # Sync demos 121 | demo_sync_workflow_json() 122 | demo_sync_api_json() 123 | 124 | # Async demos 125 | await demo_async_workflow_json() 126 | await demo_async_api_json() 127 | 128 | print("\n" + "=" * 50) 129 | print("All demos completed!") 130 | print("\nGenerated files:") 131 | print(" - demo_sync_workflow.png") 132 | print(" - demo_sync_api.png") 133 | print(" - demo_async_workflow.png") 134 | print(" - demo_async_api.png") 135 | 136 | 137 | if __name__ == "__main__": 138 | asyncio.run(main()) -------------------------------------------------------------------------------- /workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2877063d-dd57-49ce-8789-fc2ab415bd49", 3 | "revision": 0, 4 | "last_node_id": 10, 5 | "last_link_id": 10, 6 | "nodes": [ 7 | { 8 | "id": 3, 9 | "type": "KSampler", 10 | "pos": [ 11 | 863, 12 | 186 13 | ], 14 | "size": [ 15 | 315, 16 | 262 17 | ], 18 | "flags": {}, 19 | "order": 4, 20 | "mode": 0, 21 | "inputs": [ 22 | { 23 | "name": "model", 24 | "type": "MODEL", 25 | "link": 1 26 | }, 27 | { 28 | "name": "positive", 29 | "type": "CONDITIONING", 30 | "link": 4 31 | }, 32 | { 33 | "name": "negative", 34 | "type": "CONDITIONING", 35 | "link": 6 36 | }, 37 | { 38 | "name": "latent_image", 39 | "type": "LATENT", 40 | "link": 2 41 | } 42 | ], 43 | "outputs": [ 44 | { 45 | "name": "LATENT", 46 | "type": "LATENT", 47 | "slot_index": 0, 48 | "links": [ 49 | 7 50 | ] 51 | } 52 | ], 53 | "title": "KSampler", 54 | "properties": { 55 | "cnr_id": "comfy-core", 56 | "ver": "0.3.40", 57 | "Node name for S&R": "KSampler", 58 | "widget_ue_connectable": {} 59 | }, 60 | "widgets_values": [ 61 | 694907290331113, 62 | "randomize", 63 | 20, 64 | 8, 65 | "euler", 66 | "normal", 67 | 1 68 | ] 69 | }, 70 | { 71 | "id": 8, 72 | "type": "VAEDecode", 73 | "pos": [ 74 | 1209, 75 | 188 76 | ], 77 | "size": [ 78 | 210, 79 | 46 80 | ], 81 | "flags": {}, 82 | "order": 5, 83 | "mode": 0, 84 | "inputs": [ 85 | { 86 | "name": "samples", 87 | "type": "LATENT", 88 | "link": 7 89 | }, 90 | { 91 | "name": "vae", 92 | "type": "VAE", 93 | "link": 8 94 | } 95 | ], 96 | "outputs": [ 97 | { 98 | "name": "IMAGE", 99 | "type": "IMAGE", 100 | "slot_index": 0, 101 | "links": [ 102 | 10 103 | ] 104 | } 105 | ], 106 | "properties": { 107 | "cnr_id": "comfy-core", 108 | "ver": "0.3.40", 109 | "Node name for S&R": "VAEDecode", 110 | "widget_ue_connectable": {} 111 | }, 112 | "widgets_values": [] 113 | }, 114 | { 115 | "id": 10, 116 | "type": "PreviewImage", 117 | "pos": [ 118 | 1479, 119 | 187 120 | ], 121 | "size": [ 122 | 210, 123 | 26 124 | ], 125 | "flags": {}, 126 | "order": 6, 127 | "mode": 0, 128 | "inputs": [ 129 | { 130 | "name": "images", 131 | "type": "IMAGE", 132 | "link": 10 133 | } 134 | ], 135 | "outputs": [], 136 | "title": "Result Image", 137 | "properties": { 138 | "cnr_id": "comfy-core", 139 | "ver": "0.3.40", 140 | "Node name for S&R": "PreviewImage", 141 | "widget_ue_connectable": {} 142 | }, 143 | "widgets_values": [] 144 | }, 145 | { 146 | "id": 6, 147 | "type": "CLIPTextEncode", 148 | "pos": [ 149 | 416, 150 | 188 151 | ], 152 | "size": [ 153 | 422.84503173828125, 154 | 164.31304931640625 155 | ], 156 | "flags": {}, 157 | "order": 2, 158 | "mode": 0, 159 | "inputs": [ 160 | { 161 | "name": "clip", 162 | "type": "CLIP", 163 | "link": 3 164 | } 165 | ], 166 | "outputs": [ 167 | { 168 | "name": "CONDITIONING", 169 | "type": "CONDITIONING", 170 | "slot_index": 0, 171 | "links": [ 172 | 4 173 | ] 174 | } 175 | ], 176 | "title": "CLIP Text Encode Positive", 177 | "properties": { 178 | "cnr_id": "comfy-core", 179 | "ver": "0.3.40", 180 | "Node name for S&R": "CLIPTextEncode", 181 | "widget_ue_connectable": {} 182 | }, 183 | "widgets_values": [ 184 | "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," 185 | ] 186 | }, 187 | { 188 | "id": 7, 189 | "type": "CLIPTextEncode", 190 | "pos": [ 191 | 413, 192 | 389 193 | ], 194 | "size": [ 195 | 425.27801513671875, 196 | 180.6060791015625 197 | ], 198 | "flags": {}, 199 | "order": 3, 200 | "mode": 0, 201 | "inputs": [ 202 | { 203 | "name": "clip", 204 | "type": "CLIP", 205 | "link": 5 206 | } 207 | ], 208 | "outputs": [ 209 | { 210 | "name": "CONDITIONING", 211 | "type": "CONDITIONING", 212 | "slot_index": 0, 213 | "links": [ 214 | 6 215 | ] 216 | } 217 | ], 218 | "title": "CLIP Text Encode Negative", 219 | "properties": { 220 | "cnr_id": "comfy-core", 221 | "ver": "0.3.40", 222 | "Node name for S&R": "CLIPTextEncode", 223 | "widget_ue_connectable": {} 224 | }, 225 | "widgets_values": [ 226 | "text, watermark" 227 | ] 228 | }, 229 | { 230 | "id": 5, 231 | "type": "EmptyLatentImage", 232 | "pos": [ 233 | 501, 234 | 632 235 | ], 236 | "size": [ 237 | 315, 238 | 106 239 | ], 240 | "flags": {}, 241 | "order": 0, 242 | "mode": 0, 243 | "inputs": [], 244 | "outputs": [ 245 | { 246 | "name": "LATENT", 247 | "type": "LATENT", 248 | "slot_index": 0, 249 | "links": [ 250 | 2 251 | ] 252 | } 253 | ], 254 | "properties": { 255 | "cnr_id": "comfy-core", 256 | "ver": "0.3.40", 257 | "Node name for S&R": "EmptyLatentImage", 258 | "widget_ue_connectable": {} 259 | }, 260 | "widgets_values": [ 261 | 512, 262 | 512, 263 | 1 264 | ] 265 | }, 266 | { 267 | "id": 4, 268 | "type": "CheckpointLoaderSimple", 269 | "pos": [ 270 | 26, 271 | 474 272 | ], 273 | "size": [ 274 | 315, 275 | 98 276 | ], 277 | "flags": {}, 278 | "order": 1, 279 | "mode": 0, 280 | "inputs": [], 281 | "outputs": [ 282 | { 283 | "name": "MODEL", 284 | "type": "MODEL", 285 | "slot_index": 0, 286 | "links": [ 287 | 1 288 | ] 289 | }, 290 | { 291 | "name": "CLIP", 292 | "type": "CLIP", 293 | "slot_index": 1, 294 | "links": [ 295 | 3, 296 | 5 297 | ] 298 | }, 299 | { 300 | "name": "VAE", 301 | "type": "VAE", 302 | "slot_index": 2, 303 | "links": [ 304 | 8 305 | ] 306 | } 307 | ], 308 | "properties": { 309 | "cnr_id": "comfy-core", 310 | "ver": "0.3.40", 311 | "Node name for S&R": "CheckpointLoaderSimple", 312 | "widget_ue_connectable": {} 313 | }, 314 | "widgets_values": [ 315 | "illusionbreed_v20.safetensors" 316 | ] 317 | } 318 | ], 319 | "links": [ 320 | [ 321 | 1, 322 | 4, 323 | 0, 324 | 3, 325 | 0, 326 | "MODEL" 327 | ], 328 | [ 329 | 2, 330 | 5, 331 | 0, 332 | 3, 333 | 3, 334 | "LATENT" 335 | ], 336 | [ 337 | 3, 338 | 4, 339 | 1, 340 | 6, 341 | 0, 342 | "CLIP" 343 | ], 344 | [ 345 | 4, 346 | 6, 347 | 0, 348 | 3, 349 | 1, 350 | "CONDITIONING" 351 | ], 352 | [ 353 | 5, 354 | 4, 355 | 1, 356 | 7, 357 | 0, 358 | "CLIP" 359 | ], 360 | [ 361 | 6, 362 | 7, 363 | 0, 364 | 3, 365 | 2, 366 | "CONDITIONING" 367 | ], 368 | [ 369 | 7, 370 | 3, 371 | 0, 372 | 8, 373 | 0, 374 | "LATENT" 375 | ], 376 | [ 377 | 8, 378 | 4, 379 | 2, 380 | 8, 381 | 1, 382 | "VAE" 383 | ], 384 | [ 385 | 10, 386 | 8, 387 | 0, 388 | 10, 389 | 0, 390 | "IMAGE" 391 | ] 392 | ], 393 | "groups": [], 394 | "config": {}, 395 | "extra": { 396 | "ue_links": [], 397 | "ds": { 398 | "scale": 1.4818586717647073, 399 | "offset": [ 400 | -634.0426288190879, 401 | -74.44517213831327 402 | ] 403 | }, 404 | "frontendVersion": "1.22.1", 405 | "VHS_latentpreview": false, 406 | "VHS_latentpreviewrate": 0, 407 | "VHS_MetadataImage": true, 408 | "VHS_KeepIntermediate": true 409 | }, 410 | "version": 0.4 411 | } -------------------------------------------------------------------------------- /tests/test_workflow_loading.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Test automatic workflow format detection and conversion with enhanced features""" 3 | 4 | import asyncio 5 | import random 6 | import sys 7 | from comfyuiclient import ComfyUIClient, ComfyUIClientAsync 8 | 9 | # Test configuration 10 | SERVER_ADDRESS = "192.168.1.27:8188" 11 | TEST_PROMPTS = [ 12 | "beautiful mountain landscape with snow", 13 | "futuristic city at night", 14 | "magical forest with glowing plants", 15 | "underwater coral reef scene" 16 | ] 17 | 18 | def test_enhanced_features_sync(client, test_name): 19 | """Test enhanced features for sync client""" 20 | print(f" Testing enhanced features for {test_name}:") 21 | 22 | # Test reload 23 | client.reload() 24 | print(" ✓ reload() method") 25 | 26 | # Test enhanced set_data with arbitrary input 27 | client.set_data(key='KSampler', input_key='steps', input_value=25) 28 | print(" ✓ set_data with input_key/input_value") 29 | 30 | # Test number parameter 31 | client.set_data(key='EmptyLatentImage', number=128.0) 32 | print(" ✓ set_data with number parameter") 33 | 34 | # Test class_type lookup 35 | node_id = client.find_key_by_title('KSampler') 36 | print(f" ✓ find_key_by_title by class_type: {node_id}") 37 | 38 | async def test_enhanced_features_async(client, test_name): 39 | """Test enhanced features for async client""" 40 | print(f" Testing enhanced features for {test_name}:") 41 | 42 | # Test reload 43 | client.reload() 44 | print(" ✓ reload() method") 45 | 46 | # Test enhanced set_data with arbitrary input 47 | await client.set_data(key='KSampler', input_key='steps', input_value=25) 48 | print(" ✓ set_data with input_key/input_value") 49 | 50 | # Test number parameter 51 | await client.set_data(key='EmptyLatentImage', number=128.0) 52 | print(" ✓ set_data with number parameter") 53 | 54 | # Test class_type lookup 55 | node_id = client.find_key_by_title('KSampler') 56 | print(f" ✓ find_key_by_title by class_type: {node_id}") 57 | 58 | def test_sync_workflow(): 59 | """Test sync client with workflow.json (auto-conversion)""" 60 | print("\n=== Testing Sync Client with workflow.json ===") 61 | client = None 62 | try: 63 | client = ComfyUIClient(SERVER_ADDRESS, "workflow.json", debug=True) 64 | client.connect() 65 | 66 | # Test enhanced features 67 | test_enhanced_features_sync(client, "sync workflow") 68 | 69 | # Set random seed and prompt 70 | prompt = random.choice(TEST_PROMPTS) 71 | print(f"Using prompt: {prompt}") 72 | 73 | client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 74 | client.set_data(key='CLIP Text Encode Positive', text=prompt) 75 | 76 | # Generate image 77 | results = client.generate(["Result Image"]) 78 | for key, image in results.items(): 79 | filename = f"test_sync_workflow_{key}.png" 80 | image.save(filename) 81 | print(f"✓ Success! Saved: {filename}") 82 | 83 | return True 84 | except Exception as e: 85 | print(f"✗ Error: {e}") 86 | return False 87 | finally: 88 | if client: 89 | client.close() 90 | 91 | def test_sync_workflow_api(): 92 | """Test sync client with workflow_api.json (direct load)""" 93 | print("\n=== Testing Sync Client with workflow_api.json ===") 94 | client = None 95 | try: 96 | client = ComfyUIClient(SERVER_ADDRESS, "workflow_api.json", debug=True) 97 | client.connect() 98 | 99 | # Test enhanced features 100 | test_enhanced_features_sync(client, "sync API") 101 | 102 | # Set random seed and prompt 103 | prompt = random.choice(TEST_PROMPTS) 104 | print(f"Using prompt: {prompt}") 105 | 106 | client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 107 | client.set_data(key='CLIP Text Encode Positive', text=prompt) 108 | 109 | # Generate image 110 | results = client.generate(["Result Image"]) 111 | for key, image in results.items(): 112 | filename = f"test_sync_api_{key}.png" 113 | image.save(filename) 114 | print(f"✓ Success! Saved: {filename}") 115 | 116 | return True 117 | except Exception as e: 118 | print(f"✗ Error: {e}") 119 | return False 120 | finally: 121 | if client: 122 | client.close() 123 | 124 | async def test_async_workflow(): 125 | """Test async client with workflow.json (auto-conversion)""" 126 | print("\n=== Testing Async Client with workflow.json ===") 127 | client = None 128 | try: 129 | client = ComfyUIClientAsync(SERVER_ADDRESS, "workflow.json", debug=True) 130 | await client.connect() 131 | 132 | # Test enhanced features 133 | await test_enhanced_features_async(client, "async workflow") 134 | 135 | # Set random seed and prompt 136 | prompt = random.choice(TEST_PROMPTS) 137 | print(f"Using prompt: {prompt}") 138 | 139 | await client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 140 | await client.set_data(key='CLIP Text Encode Positive', text=prompt) 141 | 142 | # Generate image 143 | results = await client.generate(["Result Image"]) 144 | for key, image in results.items(): 145 | filename = f"test_async_workflow_{key}.png" 146 | image.save(filename) 147 | print(f"✓ Success! Saved: {filename}") 148 | 149 | return True 150 | except Exception as e: 151 | print(f"✗ Error: {e}") 152 | return False 153 | finally: 154 | if client: 155 | await client.close() 156 | 157 | async def test_async_workflow_api(): 158 | """Test async client with workflow_api.json (direct load)""" 159 | print("\n=== Testing Async Client with workflow_api.json ===") 160 | client = None 161 | try: 162 | client = ComfyUIClientAsync(SERVER_ADDRESS, "workflow_api.json", debug=True) 163 | await client.connect() 164 | 165 | # Test enhanced features 166 | await test_enhanced_features_async(client, "async API") 167 | 168 | # Set random seed and prompt 169 | prompt = random.choice(TEST_PROMPTS) 170 | print(f"Using prompt: {prompt}") 171 | 172 | await client.set_data(key='KSampler', seed=random.randint(0, sys.maxsize)) 173 | await client.set_data(key='CLIP Text Encode Positive', text=prompt) 174 | 175 | # Generate image 176 | results = await client.generate(["Result Image"]) 177 | for key, image in results.items(): 178 | filename = f"test_async_api_{key}.png" 179 | image.save(filename) 180 | print(f"✓ Success! Saved: {filename}") 181 | 182 | return True 183 | except Exception as e: 184 | print(f"✗ Error: {e}") 185 | return False 186 | finally: 187 | if client: 188 | await client.close() 189 | 190 | async def run_all_tests(): 191 | """Run all test cases""" 192 | print("Starting ComfyUI Client Tests") 193 | print(f"Server: {SERVER_ADDRESS}") 194 | print("-" * 50) 195 | 196 | results = {} 197 | 198 | # Test sync versions 199 | results['sync_workflow'] = test_sync_workflow() 200 | results['sync_api'] = test_sync_workflow_api() 201 | 202 | # Test async versions 203 | results['async_workflow'] = await test_async_workflow() 204 | results['async_api'] = await test_async_workflow_api() 205 | 206 | # Summary 207 | print("\n" + "=" * 60) 208 | print("TEST SUMMARY - Enhanced Features Included") 209 | print("=" * 60) 210 | 211 | all_passed = True 212 | test_descriptions = { 213 | 'sync_workflow': 'Sync Client + workflow.json + Enhanced Features', 214 | 'sync_api': 'Sync Client + workflow_api.json + Enhanced Features', 215 | 'async_workflow': 'Async Client + workflow.json + Enhanced Features', 216 | 'async_api': 'Async Client + workflow_api.json + Enhanced Features' 217 | } 218 | 219 | for test_name, passed in results.items(): 220 | status = "PASSED" if passed else "FAILED" 221 | symbol = "✓" if passed else "✗" 222 | description = test_descriptions.get(test_name, test_name) 223 | print(f"{symbol} {description}: {status}") 224 | if not passed: 225 | all_passed = False 226 | 227 | print("\nTested Enhanced Features:") 228 | print(" ✓ Debug mode support") 229 | print(" ✓ reload() method") 230 | print(" ✓ Enhanced set_data() with input_key/input_value") 231 | print(" ✓ set_data() with number parameter") 232 | print(" ✓ find_key_by_title() with class_type lookup") 233 | print(" ✓ Automatic workflow.json to API conversion") 234 | 235 | print("\n" + ("🎉 All tests passed! Both sync and async versions have equivalent functionality." 236 | if all_passed else "❌ Some tests failed!")) 237 | return all_passed 238 | 239 | if __name__ == "__main__": 240 | # Run all tests 241 | all_passed = asyncio.run(run_all_tests()) 242 | sys.exit(0 if all_passed else 1) -------------------------------------------------------------------------------- /test_env/bin/Activate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Activate a Python virtual environment for the current PowerShell session. 4 | 5 | .Description 6 | Pushes the python executable for a virtual environment to the front of the 7 | $Env:PATH environment variable and sets the prompt to signify that you are 8 | in a Python virtual environment. Makes use of the command line switches as 9 | well as the `pyvenv.cfg` file values present in the virtual environment. 10 | 11 | .Parameter VenvDir 12 | Path to the directory that contains the virtual environment to activate. The 13 | default value for this is the parent of the directory that the Activate.ps1 14 | script is located within. 15 | 16 | .Parameter Prompt 17 | The prompt prefix to display when this virtual environment is activated. By 18 | default, this prompt is the name of the virtual environment folder (VenvDir) 19 | surrounded by parentheses and followed by a single space (ie. '(.venv) '). 20 | 21 | .Example 22 | Activate.ps1 23 | Activates the Python virtual environment that contains the Activate.ps1 script. 24 | 25 | .Example 26 | Activate.ps1 -Verbose 27 | Activates the Python virtual environment that contains the Activate.ps1 script, 28 | and shows extra information about the activation as it executes. 29 | 30 | .Example 31 | Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv 32 | Activates the Python virtual environment located in the specified location. 33 | 34 | .Example 35 | Activate.ps1 -Prompt "MyPython" 36 | Activates the Python virtual environment that contains the Activate.ps1 script, 37 | and prefixes the current prompt with the specified string (surrounded in 38 | parentheses) while the virtual environment is active. 39 | 40 | .Notes 41 | On Windows, it may be required to enable this Activate.ps1 script by setting the 42 | execution policy for the user. You can do this by issuing the following PowerShell 43 | command: 44 | 45 | PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 46 | 47 | For more information on Execution Policies: 48 | https://go.microsoft.com/fwlink/?LinkID=135170 49 | 50 | #> 51 | Param( 52 | [Parameter(Mandatory = $false)] 53 | [String] 54 | $VenvDir, 55 | [Parameter(Mandatory = $false)] 56 | [String] 57 | $Prompt 58 | ) 59 | 60 | <# Function declarations --------------------------------------------------- #> 61 | 62 | <# 63 | .Synopsis 64 | Remove all shell session elements added by the Activate script, including the 65 | addition of the virtual environment's Python executable from the beginning of 66 | the PATH variable. 67 | 68 | .Parameter NonDestructive 69 | If present, do not remove this function from the global namespace for the 70 | session. 71 | 72 | #> 73 | function global:deactivate ([switch]$NonDestructive) { 74 | # Revert to original values 75 | 76 | # The prior prompt: 77 | if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { 78 | Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt 79 | Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT 80 | } 81 | 82 | # The prior PYTHONHOME: 83 | if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { 84 | Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME 85 | Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME 86 | } 87 | 88 | # The prior PATH: 89 | if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { 90 | Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH 91 | Remove-Item -Path Env:_OLD_VIRTUAL_PATH 92 | } 93 | 94 | # Just remove the VIRTUAL_ENV altogether: 95 | if (Test-Path -Path Env:VIRTUAL_ENV) { 96 | Remove-Item -Path env:VIRTUAL_ENV 97 | } 98 | 99 | # Just remove VIRTUAL_ENV_PROMPT altogether. 100 | if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { 101 | Remove-Item -Path env:VIRTUAL_ENV_PROMPT 102 | } 103 | 104 | # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: 105 | if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { 106 | Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force 107 | } 108 | 109 | # Leave deactivate function in the global namespace if requested: 110 | if (-not $NonDestructive) { 111 | Remove-Item -Path function:deactivate 112 | } 113 | } 114 | 115 | <# 116 | .Description 117 | Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the 118 | given folder, and returns them in a map. 119 | 120 | For each line in the pyvenv.cfg file, if that line can be parsed into exactly 121 | two strings separated by `=` (with any amount of whitespace surrounding the =) 122 | then it is considered a `key = value` line. The left hand string is the key, 123 | the right hand is the value. 124 | 125 | If the value starts with a `'` or a `"` then the first and last character is 126 | stripped from the value before being captured. 127 | 128 | .Parameter ConfigDir 129 | Path to the directory that contains the `pyvenv.cfg` file. 130 | #> 131 | function Get-PyVenvConfig( 132 | [String] 133 | $ConfigDir 134 | ) { 135 | Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" 136 | 137 | # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). 138 | $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue 139 | 140 | # An empty map will be returned if no config file is found. 141 | $pyvenvConfig = @{ } 142 | 143 | if ($pyvenvConfigPath) { 144 | 145 | Write-Verbose "File exists, parse `key = value` lines" 146 | $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath 147 | 148 | $pyvenvConfigContent | ForEach-Object { 149 | $keyval = $PSItem -split "\s*=\s*", 2 150 | if ($keyval[0] -and $keyval[1]) { 151 | $val = $keyval[1] 152 | 153 | # Remove extraneous quotations around a string value. 154 | if ("'""".Contains($val.Substring(0, 1))) { 155 | $val = $val.Substring(1, $val.Length - 2) 156 | } 157 | 158 | $pyvenvConfig[$keyval[0]] = $val 159 | Write-Verbose "Adding Key: '$($keyval[0])'='$val'" 160 | } 161 | } 162 | } 163 | return $pyvenvConfig 164 | } 165 | 166 | 167 | <# Begin Activate script --------------------------------------------------- #> 168 | 169 | # Determine the containing directory of this script 170 | $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition 171 | $VenvExecDir = Get-Item -Path $VenvExecPath 172 | 173 | Write-Verbose "Activation script is located in path: '$VenvExecPath'" 174 | Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" 175 | Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" 176 | 177 | # Set values required in priority: CmdLine, ConfigFile, Default 178 | # First, get the location of the virtual environment, it might not be 179 | # VenvExecDir if specified on the command line. 180 | if ($VenvDir) { 181 | Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" 182 | } 183 | else { 184 | Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." 185 | $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") 186 | Write-Verbose "VenvDir=$VenvDir" 187 | } 188 | 189 | # Next, read the `pyvenv.cfg` file to determine any required value such 190 | # as `prompt`. 191 | $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir 192 | 193 | # Next, set the prompt from the command line, or the config file, or 194 | # just use the name of the virtual environment folder. 195 | if ($Prompt) { 196 | Write-Verbose "Prompt specified as argument, using '$Prompt'" 197 | } 198 | else { 199 | Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" 200 | if ($pyvenvCfg -and $pyvenvCfg['prompt']) { 201 | Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" 202 | $Prompt = $pyvenvCfg['prompt']; 203 | } 204 | else { 205 | Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" 206 | Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" 207 | $Prompt = Split-Path -Path $venvDir -Leaf 208 | } 209 | } 210 | 211 | Write-Verbose "Prompt = '$Prompt'" 212 | Write-Verbose "VenvDir='$VenvDir'" 213 | 214 | # Deactivate any currently active virtual environment, but leave the 215 | # deactivate function in place. 216 | deactivate -nondestructive 217 | 218 | # Now set the environment variable VIRTUAL_ENV, used by many tools to determine 219 | # that there is an activated venv. 220 | $env:VIRTUAL_ENV = $VenvDir 221 | 222 | if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { 223 | 224 | Write-Verbose "Setting prompt to '$Prompt'" 225 | 226 | # Set the prompt to include the env name 227 | # Make sure _OLD_VIRTUAL_PROMPT is global 228 | function global:_OLD_VIRTUAL_PROMPT { "" } 229 | Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT 230 | New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt 231 | 232 | function global:prompt { 233 | Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " 234 | _OLD_VIRTUAL_PROMPT 235 | } 236 | $env:VIRTUAL_ENV_PROMPT = $Prompt 237 | } 238 | 239 | # Clear PYTHONHOME 240 | if (Test-Path -Path Env:PYTHONHOME) { 241 | Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME 242 | Remove-Item -Path Env:PYTHONHOME 243 | } 244 | 245 | # Add the venv to the PATH 246 | Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH 247 | $Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" 248 | -------------------------------------------------------------------------------- /test_venv/bin/Activate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Activate a Python virtual environment for the current PowerShell session. 4 | 5 | .Description 6 | Pushes the python executable for a virtual environment to the front of the 7 | $Env:PATH environment variable and sets the prompt to signify that you are 8 | in a Python virtual environment. Makes use of the command line switches as 9 | well as the `pyvenv.cfg` file values present in the virtual environment. 10 | 11 | .Parameter VenvDir 12 | Path to the directory that contains the virtual environment to activate. The 13 | default value for this is the parent of the directory that the Activate.ps1 14 | script is located within. 15 | 16 | .Parameter Prompt 17 | The prompt prefix to display when this virtual environment is activated. By 18 | default, this prompt is the name of the virtual environment folder (VenvDir) 19 | surrounded by parentheses and followed by a single space (ie. '(.venv) '). 20 | 21 | .Example 22 | Activate.ps1 23 | Activates the Python virtual environment that contains the Activate.ps1 script. 24 | 25 | .Example 26 | Activate.ps1 -Verbose 27 | Activates the Python virtual environment that contains the Activate.ps1 script, 28 | and shows extra information about the activation as it executes. 29 | 30 | .Example 31 | Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv 32 | Activates the Python virtual environment located in the specified location. 33 | 34 | .Example 35 | Activate.ps1 -Prompt "MyPython" 36 | Activates the Python virtual environment that contains the Activate.ps1 script, 37 | and prefixes the current prompt with the specified string (surrounded in 38 | parentheses) while the virtual environment is active. 39 | 40 | .Notes 41 | On Windows, it may be required to enable this Activate.ps1 script by setting the 42 | execution policy for the user. You can do this by issuing the following PowerShell 43 | command: 44 | 45 | PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 46 | 47 | For more information on Execution Policies: 48 | https://go.microsoft.com/fwlink/?LinkID=135170 49 | 50 | #> 51 | Param( 52 | [Parameter(Mandatory = $false)] 53 | [String] 54 | $VenvDir, 55 | [Parameter(Mandatory = $false)] 56 | [String] 57 | $Prompt 58 | ) 59 | 60 | <# Function declarations --------------------------------------------------- #> 61 | 62 | <# 63 | .Synopsis 64 | Remove all shell session elements added by the Activate script, including the 65 | addition of the virtual environment's Python executable from the beginning of 66 | the PATH variable. 67 | 68 | .Parameter NonDestructive 69 | If present, do not remove this function from the global namespace for the 70 | session. 71 | 72 | #> 73 | function global:deactivate ([switch]$NonDestructive) { 74 | # Revert to original values 75 | 76 | # The prior prompt: 77 | if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { 78 | Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt 79 | Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT 80 | } 81 | 82 | # The prior PYTHONHOME: 83 | if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { 84 | Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME 85 | Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME 86 | } 87 | 88 | # The prior PATH: 89 | if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { 90 | Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH 91 | Remove-Item -Path Env:_OLD_VIRTUAL_PATH 92 | } 93 | 94 | # Just remove the VIRTUAL_ENV altogether: 95 | if (Test-Path -Path Env:VIRTUAL_ENV) { 96 | Remove-Item -Path env:VIRTUAL_ENV 97 | } 98 | 99 | # Just remove VIRTUAL_ENV_PROMPT altogether. 100 | if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { 101 | Remove-Item -Path env:VIRTUAL_ENV_PROMPT 102 | } 103 | 104 | # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: 105 | if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { 106 | Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force 107 | } 108 | 109 | # Leave deactivate function in the global namespace if requested: 110 | if (-not $NonDestructive) { 111 | Remove-Item -Path function:deactivate 112 | } 113 | } 114 | 115 | <# 116 | .Description 117 | Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the 118 | given folder, and returns them in a map. 119 | 120 | For each line in the pyvenv.cfg file, if that line can be parsed into exactly 121 | two strings separated by `=` (with any amount of whitespace surrounding the =) 122 | then it is considered a `key = value` line. The left hand string is the key, 123 | the right hand is the value. 124 | 125 | If the value starts with a `'` or a `"` then the first and last character is 126 | stripped from the value before being captured. 127 | 128 | .Parameter ConfigDir 129 | Path to the directory that contains the `pyvenv.cfg` file. 130 | #> 131 | function Get-PyVenvConfig( 132 | [String] 133 | $ConfigDir 134 | ) { 135 | Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" 136 | 137 | # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). 138 | $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue 139 | 140 | # An empty map will be returned if no config file is found. 141 | $pyvenvConfig = @{ } 142 | 143 | if ($pyvenvConfigPath) { 144 | 145 | Write-Verbose "File exists, parse `key = value` lines" 146 | $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath 147 | 148 | $pyvenvConfigContent | ForEach-Object { 149 | $keyval = $PSItem -split "\s*=\s*", 2 150 | if ($keyval[0] -and $keyval[1]) { 151 | $val = $keyval[1] 152 | 153 | # Remove extraneous quotations around a string value. 154 | if ("'""".Contains($val.Substring(0, 1))) { 155 | $val = $val.Substring(1, $val.Length - 2) 156 | } 157 | 158 | $pyvenvConfig[$keyval[0]] = $val 159 | Write-Verbose "Adding Key: '$($keyval[0])'='$val'" 160 | } 161 | } 162 | } 163 | return $pyvenvConfig 164 | } 165 | 166 | 167 | <# Begin Activate script --------------------------------------------------- #> 168 | 169 | # Determine the containing directory of this script 170 | $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition 171 | $VenvExecDir = Get-Item -Path $VenvExecPath 172 | 173 | Write-Verbose "Activation script is located in path: '$VenvExecPath'" 174 | Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" 175 | Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" 176 | 177 | # Set values required in priority: CmdLine, ConfigFile, Default 178 | # First, get the location of the virtual environment, it might not be 179 | # VenvExecDir if specified on the command line. 180 | if ($VenvDir) { 181 | Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" 182 | } 183 | else { 184 | Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." 185 | $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") 186 | Write-Verbose "VenvDir=$VenvDir" 187 | } 188 | 189 | # Next, read the `pyvenv.cfg` file to determine any required value such 190 | # as `prompt`. 191 | $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir 192 | 193 | # Next, set the prompt from the command line, or the config file, or 194 | # just use the name of the virtual environment folder. 195 | if ($Prompt) { 196 | Write-Verbose "Prompt specified as argument, using '$Prompt'" 197 | } 198 | else { 199 | Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" 200 | if ($pyvenvCfg -and $pyvenvCfg['prompt']) { 201 | Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" 202 | $Prompt = $pyvenvCfg['prompt']; 203 | } 204 | else { 205 | Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" 206 | Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" 207 | $Prompt = Split-Path -Path $venvDir -Leaf 208 | } 209 | } 210 | 211 | Write-Verbose "Prompt = '$Prompt'" 212 | Write-Verbose "VenvDir='$VenvDir'" 213 | 214 | # Deactivate any currently active virtual environment, but leave the 215 | # deactivate function in place. 216 | deactivate -nondestructive 217 | 218 | # Now set the environment variable VIRTUAL_ENV, used by many tools to determine 219 | # that there is an activated venv. 220 | $env:VIRTUAL_ENV = $VenvDir 221 | 222 | if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { 223 | 224 | Write-Verbose "Setting prompt to '$Prompt'" 225 | 226 | # Set the prompt to include the env name 227 | # Make sure _OLD_VIRTUAL_PROMPT is global 228 | function global:_OLD_VIRTUAL_PROMPT { "" } 229 | Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT 230 | New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt 231 | 232 | function global:prompt { 233 | Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " 234 | _OLD_VIRTUAL_PROMPT 235 | } 236 | $env:VIRTUAL_ENV_PROMPT = $Prompt 237 | } 238 | 239 | # Clear PYTHONHOME 240 | if (Test-Path -Path Env:PYTHONHOME) { 241 | Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME 242 | Remove-Item -Path Env:PYTHONHOME 243 | } 244 | 245 | # Add the venv to the PATH 246 | Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH 247 | $Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI API Client 2 | 3 | A Python client library for interacting with ComfyUI via its API. Supports both synchronous and asynchronous operations with automatic workflow format conversion. 4 | 5 | ## Features 6 | 7 | - 🔄 **Dual Client Support**: Both sync (`ComfyUIClient`) and async (`ComfyUIClientAsync`) implementations 8 | - 🎯 **Automatic Format Detection**: Automatically converts `workflow.json` to API format 9 | - 🛠️ **Enhanced Configuration**: Flexible `set_data()` method for all parameter types 10 | - 🐛 **Debug Mode**: Optional debug output for development and troubleshooting 11 | - 🔧 **Dynamic Reload**: Reload workflow files without restarting 12 | - 🛡️ **Robust Error Handling**: Comprehensive error handling with user-friendly messages 13 | - 🔍 **Smart Node Lookup**: Find nodes by title or class_type 14 | - 📦 **Image Upload Support**: Direct image upload to ComfyUI server 15 | 16 | ## Installation 17 | 18 | ```bash 19 | pip install comfyui-workflow-client 20 | ``` 21 | 22 | ### Requirements 23 | 24 | ``` 25 | requests 26 | aiohttp 27 | Pillow 28 | ``` 29 | 30 | ## Quick Start 31 | 32 | ### Synchronous Client 33 | 34 | ```python 35 | from comfyuiclient import ComfyUIClient 36 | 37 | # Initialize client (supports both workflow.json and workflow_api.json) 38 | client = ComfyUIClient("localhost:8188", "workflow.json") 39 | client.connect() 40 | 41 | # Set parameters 42 | client.set_data(key='KSampler', seed=12345) 43 | client.set_data(key='CLIP Text Encode Positive', text="beautiful landscape") 44 | 45 | # Generate images 46 | results = client.generate(["Result Image"]) 47 | for key, image in results.items(): 48 | image.save(f"{key}.png") 49 | 50 | client.close() 51 | ``` 52 | 53 | ### Asynchronous Client 54 | 55 | ```python 56 | import asyncio 57 | from comfyuiclient import ComfyUIClientAsync 58 | 59 | async def main(): 60 | # Initialize async client 61 | client = ComfyUIClientAsync("localhost:8188", "workflow.json") 62 | await client.connect() 63 | 64 | # Set parameters (all async) 65 | await client.set_data(key='KSampler', seed=12345) 66 | await client.set_data(key='CLIP Text Encode Positive', text="beautiful landscape") 67 | 68 | # Generate images 69 | results = await client.generate(["Result Image"]) 70 | for key, image in results.items(): 71 | image.save(f"{key}.png") 72 | 73 | await client.close() 74 | 75 | asyncio.run(main()) 76 | ``` 77 | 78 | ## API Reference 79 | 80 | ### Client Initialization 81 | 82 | ```python 83 | # Basic initialization 84 | client = ComfyUIClient(server_address, workflow_file) 85 | 86 | # With debug mode 87 | client = ComfyUIClient(server_address, workflow_file, debug=True) 88 | ``` 89 | 90 | **Parameters:** 91 | - `server_address`: ComfyUI server address (e.g., "localhost:8188") 92 | - `workflow_file`: Path to workflow.json or workflow_api.json 93 | - `debug`: Enable debug output (default: False) 94 | 95 | ### Core Methods 96 | 97 | #### `connect()` 98 | Establishes connection to ComfyUI server. 99 | 100 | ```python 101 | # Sync 102 | client.connect() 103 | 104 | # Async 105 | await client.connect() 106 | ``` 107 | 108 | #### `set_data(key, **kwargs)` 109 | Sets parameters for workflow nodes. 110 | 111 | ```python 112 | # Basic parameters 113 | client.set_data(key='KSampler', seed=12345) 114 | client.set_data(key='CLIP Text Encode Positive', text="prompt text") 115 | 116 | # Advanced parameters 117 | client.set_data(key='KSampler', input_key='steps', input_value=25) 118 | client.set_data(key='EmptyLatentImage', number=512.0) 119 | client.set_data(key='SomeNode', value=1.5) 120 | 121 | # Image upload 122 | from PIL import Image 123 | image = Image.open("input.png") 124 | client.set_data(key='LoadImage', image=image) 125 | ``` 126 | 127 | **Parameters:** 128 | - `key`: Node title or class_type 129 | - `text`: Text input for text nodes 130 | - `seed`: Seed value for generation nodes 131 | - `image`: PIL Image object for image inputs 132 | - `number`: Numeric parameter (mapped to 'Number' input) 133 | - `value`: Numeric parameter (mapped to 'value' input) 134 | - `input_key`/`input_value`: Arbitrary key-value pairs 135 | 136 | #### `generate(node_names=None)` 137 | Generates outputs from specified nodes. 138 | 139 | ```python 140 | # Generate from specific nodes 141 | results = client.generate(["Result Image", "Preview"]) 142 | 143 | # Generate from all output nodes 144 | results = client.generate() 145 | 146 | # Results are returned as {node_name: PIL.Image} dictionary 147 | for node_name, image in results.items(): 148 | image.save(f"{node_name}.png") 149 | ``` 150 | 151 | #### `reload()` 152 | Reloads the workflow file (useful for dynamic workflows). 153 | 154 | ```python 155 | client.reload() 156 | ``` 157 | 158 | #### `close()` 159 | Closes the connection and cleans up resources. 160 | 161 | ```python 162 | # Sync 163 | client.close() 164 | 165 | # Async 166 | await client.close() 167 | ``` 168 | 169 | ### Utility Functions 170 | 171 | #### `convert_workflow_to_api(workflow_json)` 172 | Converts ComfyUI workflow format to API format. 173 | 174 | ```python 175 | from comfyuiclient import convert_workflow_to_api 176 | 177 | # Convert file 178 | api_format = convert_workflow_to_api("workflow.json") 179 | 180 | # Convert dict 181 | with open("workflow.json") as f: 182 | workflow_data = json.load(f) 183 | api_format = convert_workflow_to_api(workflow_data) 184 | ``` 185 | 186 | ## Workflow File Support 187 | 188 | The client automatically detects and handles both workflow formats: 189 | 190 | ### workflow.json (ComfyUI Editor Format) 191 | - Exported from ComfyUI web interface 192 | - Contains UI layout and visual information 193 | - **Automatically converted** to API format 194 | 195 | ### workflow_api.json (ComfyUI API Format) 196 | - API-ready format 197 | - **Used directly** without conversion 198 | 199 | Example of automatic detection: 200 | ```python 201 | # Both work seamlessly 202 | client1 = ComfyUIClient("localhost:8188", "workflow.json") # Auto-converted 203 | client2 = ComfyUIClient("localhost:8188", "workflow_api.json") # Direct use 204 | ``` 205 | 206 | ## Error Handling 207 | 208 | The client provides comprehensive error handling: 209 | 210 | ```python 211 | try: 212 | client = ComfyUIClient("localhost:8188", "workflow.json") 213 | client.connect() 214 | results = client.generate(["Result Image"]) 215 | except ConnectionError as e: 216 | print(f"Connection failed: {e}") 217 | except ValueError as e: 218 | print(f"Invalid data: {e}") 219 | except TimeoutError as e: 220 | print(f"Operation timed out: {e}") 221 | except Exception as e: 222 | print(f"Unexpected error: {e}") 223 | finally: 224 | client.close() 225 | ``` 226 | 227 | ## Debug Mode 228 | 229 | Enable debug mode for detailed logging: 230 | 231 | ```python 232 | client = ComfyUIClient("localhost:8188", "workflow.json", debug=True) 233 | ``` 234 | 235 | Debug output includes: 236 | - Workflow loading status 237 | - Parameter setting details 238 | - Node lookup information 239 | - Error details and retry attempts 240 | 241 | ## Advanced Examples 242 | 243 | ### Context Manager Pattern 244 | 245 | ```python 246 | class ComfyUIContextManager: 247 | def __init__(self, *args, **kwargs): 248 | self.client = ComfyUIClient(*args, **kwargs) 249 | 250 | def __enter__(self): 251 | self.client.connect() 252 | return self.client 253 | 254 | def __exit__(self, exc_type, exc_val, exc_tb): 255 | self.client.close() 256 | 257 | # Usage 258 | with ComfyUIContextManager("localhost:8188", "workflow.json") as client: 259 | client.set_data(key='KSampler', seed=12345) 260 | results = client.generate(["Result Image"]) 261 | ``` 262 | 263 | ### Batch Processing 264 | 265 | ```python 266 | import random 267 | 268 | prompts = ["sunset over mountains", "city at night", "forest lake"] 269 | seeds = [random.randint(0, 2**32) for _ in range(3)] 270 | 271 | client = ComfyUIClient("localhost:8188", "workflow.json") 272 | client.connect() 273 | 274 | for i, (prompt, seed) in enumerate(zip(prompts, seeds)): 275 | client.set_data(key='CLIP Text Encode Positive', text=prompt) 276 | client.set_data(key='KSampler', seed=seed) 277 | 278 | results = client.generate(["Result Image"]) 279 | for key, image in results.items(): 280 | image.save(f"output_{i}_{key}.png") 281 | 282 | client.close() 283 | ``` 284 | 285 | ### Dynamic Workflow Updates 286 | 287 | ```python 288 | client = ComfyUIClient("localhost:8188", "workflow.json") 289 | client.connect() 290 | 291 | # Initial generation 292 | client.set_data(key='KSampler', seed=12345) 293 | results = client.generate(["Result Image"]) 294 | 295 | # Modify workflow file externally, then reload 296 | client.reload() 297 | 298 | # Use updated workflow 299 | client.set_data(key='KSampler', seed=67890) 300 | results = client.generate(["Result Image"]) 301 | 302 | client.close() 303 | ``` 304 | 305 | ## Testing 306 | 307 | Run the test suite: 308 | 309 | ```bash 310 | # Basic functionality tests 311 | python test_workflow_loading.py 312 | 313 | # Error handling tests 314 | python test_error_handling.py 315 | 316 | # Enhanced features tests 317 | python test_enhanced_features.py 318 | 319 | # Format conversion tests 320 | python test_conversion.py 321 | ``` 322 | 323 | ## Troubleshooting 324 | 325 | ### Common Issues 326 | 327 | **1. Connection Refused** 328 | ``` 329 | ConnectionError: Failed to connect to ComfyUI server 330 | ``` 331 | - Ensure ComfyUI is running on the specified address 332 | - Check firewall settings 333 | - Verify the port number 334 | 335 | **2. Key Not Found** 336 | ``` 337 | Key not found: NodeName 338 | ``` 339 | - Check node title in ComfyUI interface 340 | - Try using class_type instead of title 341 | - Enable debug mode to see available nodes 342 | 343 | **3. Timeout Errors** 344 | ``` 345 | TimeoutError: Timeout waiting for prompt to complete 346 | ``` 347 | - Complex workflows may take longer than 5 minutes 348 | - Check ComfyUI server performance 349 | - Verify workflow is valid 350 | 351 | ### Debug Tips 352 | 353 | 1. **Enable debug mode** for detailed logs: 354 | ```python 355 | client = ComfyUIClient("localhost:8188", "workflow.json", debug=True) 356 | ``` 357 | 358 | 2. **Check node names** in your workflow: 359 | ```python 360 | client = ComfyUIClient("localhost:8188", "workflow.json", debug=True) 361 | # Debug output will show available node IDs and titles 362 | ``` 363 | 364 | 3. **Test workflow in ComfyUI first** before using the client 365 | 366 | 4. **Use format conversion** to understand your workflow: 367 | ```python 368 | api_format = convert_workflow_to_api("workflow.json") 369 | print(json.dumps(api_format, indent=2)) 370 | ``` 371 | 372 | ## License 373 | 374 | This project is licensed under the MIT License - see the LICENSE file for details. 375 | 376 | ## Contributing 377 | 378 | 1. Fork the repository 379 | 2. Create a feature branch 380 | 3. Add tests for new functionality 381 | 4. Ensure all tests pass 382 | 5. Submit a pull request 383 | 384 | ## Changelog 385 | 386 | ### Latest Version 387 | - ✅ Enhanced error handling with specific exception types 388 | - ✅ Debug mode for development and troubleshooting 389 | - ✅ Automatic workflow.json to API format conversion 390 | - ✅ Dynamic workflow reloading 391 | - ✅ Enhanced set_data() with arbitrary parameter support 392 | - ✅ Smart node lookup by title or class_type 393 | - ✅ Comprehensive test suite 394 | - ✅ Timeout handling for long-running operations 395 | - ✅ Robust resource cleanup 396 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # ComfyUI API クライアント 2 | 3 | ComfyUI API との連携を行うPythonクライアントライブラリです。同期・非同期両方の操作をサポートし、ワークフローフォーマットの自動変換機能を備えています。 4 | 5 | ## 特徴 6 | 7 | - 🔄 **デュアルクライアント対応**: 同期(`ComfyUIClient`)と非同期(`ComfyUIClientAsync`)の両実装 8 | - 🎯 **自動フォーマット検出**: `workflow.json`を自動的にAPIフォーマットに変換 9 | - 🛠️ **拡張設定機能**: あらゆるパラメータタイプに対応した柔軟な`set_data()`メソッド 10 | - 🐛 **デバッグモード**: 開発とトラブルシューティング用のオプションデバッグ出力 11 | - 🔧 **動的リロード**: 再起動なしでワークフローファイルをリロード 12 | - 🛡️ **堅牢なエラーハンドリング**: ユーザーフレンドリーなメッセージを持つ包括的エラー処理 13 | - 🔍 **スマートノード検索**: タイトルまたはclass_typeでノードを検索 14 | - 📦 **画像アップロード対応**: ComfyUIサーバーへの直接画像アップロード 15 | 16 | ## インストール 17 | 18 | ```bash 19 | pip install comfyui-workflow-client 20 | ``` 21 | 22 | ### 依存関係 23 | 24 | ``` 25 | requests 26 | aiohttp 27 | Pillow 28 | ``` 29 | 30 | ## クイックスタート 31 | 32 | ### 同期クライアント 33 | 34 | ```python 35 | from comfyuiclient import ComfyUIClient 36 | 37 | # クライアントを初期化(workflow.jsonとworkflow_api.jsonの両方に対応) 38 | client = ComfyUIClient("localhost:8188", "workflow.json") 39 | client.connect() 40 | 41 | # パラメータを設定 42 | client.set_data(key='KSampler', seed=12345) 43 | client.set_data(key='CLIP Text Encode Positive', text="美しい風景") 44 | 45 | # 画像を生成 46 | results = client.generate(["Result Image"]) 47 | for key, image in results.items(): 48 | image.save(f"{key}.png") 49 | 50 | client.close() 51 | ``` 52 | 53 | ### 非同期クライアント 54 | 55 | ```python 56 | import asyncio 57 | from comfyuiclient import ComfyUIClientAsync 58 | 59 | async def main(): 60 | # 非同期クライアントを初期化 61 | client = ComfyUIClientAsync("localhost:8188", "workflow.json") 62 | await client.connect() 63 | 64 | # パラメータを設定(すべて非同期) 65 | await client.set_data(key='KSampler', seed=12345) 66 | await client.set_data(key='CLIP Text Encode Positive', text="美しい風景") 67 | 68 | # 画像を生成 69 | results = await client.generate(["Result Image"]) 70 | for key, image in results.items(): 71 | image.save(f"{key}.png") 72 | 73 | await client.close() 74 | 75 | asyncio.run(main()) 76 | ``` 77 | 78 | ## APIリファレンス 79 | 80 | ### クライアントの初期化 81 | 82 | ```python 83 | # 基本的な初期化 84 | client = ComfyUIClient(server_address, workflow_file) 85 | 86 | # デバッグモード付き 87 | client = ComfyUIClient(server_address, workflow_file, debug=True) 88 | ``` 89 | 90 | **パラメータ:** 91 | - `server_address`: ComfyUIサーバーのアドレス(例:"localhost:8188") 92 | - `workflow_file`: workflow.jsonまたはworkflow_api.jsonのパス 93 | - `debug`: デバッグ出力を有効にする(デフォルト:False) 94 | 95 | ### 主要メソッド 96 | 97 | #### `connect()` 98 | ComfyUIサーバーへの接続を確立します。 99 | 100 | ```python 101 | # 同期 102 | client.connect() 103 | 104 | # 非同期 105 | await client.connect() 106 | ``` 107 | 108 | #### `set_data(key, **kwargs)` 109 | ワークフローノードのパラメータを設定します。 110 | 111 | ```python 112 | # 基本パラメータ 113 | client.set_data(key='KSampler', seed=12345) 114 | client.set_data(key='CLIP Text Encode Positive', text="プロンプトテキスト") 115 | 116 | # 高度なパラメータ 117 | client.set_data(key='KSampler', input_key='steps', input_value=25) 118 | client.set_data(key='EmptyLatentImage', number=512.0) 119 | client.set_data(key='SomeNode', value=1.5) 120 | 121 | # 画像アップロード 122 | from PIL import Image 123 | image = Image.open("input.png") 124 | client.set_data(key='LoadImage', image=image) 125 | ``` 126 | 127 | **パラメータ:** 128 | - `key`: ノードタイトルまたはclass_type 129 | - `text`: テキストノード用のテキスト入力 130 | - `seed`: 生成ノード用のシード値 131 | - `image`: 画像入力用のPIL Imageオブジェクト 132 | - `number`: 数値パラメータ('Number'入力にマップ) 133 | - `value`: 数値パラメータ('value'入力にマップ) 134 | - `input_key`/`input_value`: 任意のキー値ペア 135 | 136 | #### `generate(node_names=None)` 137 | 指定されたノードから出力を生成します。 138 | 139 | ```python 140 | # 特定のノードから生成 141 | results = client.generate(["Result Image", "Preview"]) 142 | 143 | # すべての出力ノードから生成 144 | results = client.generate() 145 | 146 | # 結果は{node_name: PIL.Image}辞書として返される 147 | for node_name, image in results.items(): 148 | image.save(f"{node_name}.png") 149 | ``` 150 | 151 | #### `reload()` 152 | ワークフローファイルをリロードします(動的ワークフローに便利)。 153 | 154 | ```python 155 | client.reload() 156 | ``` 157 | 158 | #### `close()` 159 | 接続を閉じ、リソースをクリーンアップします。 160 | 161 | ```python 162 | # 同期 163 | client.close() 164 | 165 | # 非同期 166 | await client.close() 167 | ``` 168 | 169 | ### ユーティリティ関数 170 | 171 | #### `convert_workflow_to_api(workflow_json)` 172 | ComfyUIワークフローフォーマットをAPIフォーマットに変換します。 173 | 174 | ```python 175 | from comfyuiclient import convert_workflow_to_api 176 | 177 | # ファイルを変換 178 | api_format = convert_workflow_to_api("workflow.json") 179 | 180 | # 辞書を変換 181 | with open("workflow.json") as f: 182 | workflow_data = json.load(f) 183 | api_format = convert_workflow_to_api(workflow_data) 184 | ``` 185 | 186 | ## ワークフローファイル対応 187 | 188 | クライアントは両方のワークフローフォーマットを自動検出して処理します: 189 | 190 | ### workflow.json(ComfyUIエディタフォーマット) 191 | - ComfyUI Webインターフェースからエクスポート 192 | - UIレイアウトと視覚情報を含む 193 | - **自動的にAPIフォーマットに変換** 194 | 195 | ### workflow_api.json(ComfyUI APIフォーマット) 196 | - API対応フォーマット 197 | - **変換なしで直接使用** 198 | 199 | 自動検出の例: 200 | ```python 201 | # どちらもシームレスに動作 202 | client1 = ComfyUIClient("localhost:8188", "workflow.json") # 自動変換 203 | client2 = ComfyUIClient("localhost:8188", "workflow_api.json") # 直接使用 204 | ``` 205 | 206 | ## エラーハンドリング 207 | 208 | クライアントは包括的なエラーハンドリングを提供します: 209 | 210 | ```python 211 | try: 212 | client = ComfyUIClient("localhost:8188", "workflow.json") 213 | client.connect() 214 | results = client.generate(["Result Image"]) 215 | except ConnectionError as e: 216 | print(f"接続に失敗しました: {e}") 217 | except ValueError as e: 218 | print(f"無効なデータです: {e}") 219 | except TimeoutError as e: 220 | print(f"操作がタイムアウトしました: {e}") 221 | except Exception as e: 222 | print(f"予期しないエラー: {e}") 223 | finally: 224 | client.close() 225 | ``` 226 | 227 | ## デバッグモード 228 | 229 | 詳細なログ出力を行うためにデバッグモードを有効にします: 230 | 231 | ```python 232 | client = ComfyUIClient("localhost:8188", "workflow.json", debug=True) 233 | ``` 234 | 235 | デバッグ出力に含まれるもの: 236 | - ワークフロー読み込みステータス 237 | - パラメータ設定の詳細 238 | - ノード検索情報 239 | - エラーの詳細とリトライ試行 240 | 241 | ## 高度な使用例 242 | 243 | ### コンテキストマネージャーパターン 244 | 245 | ```python 246 | class ComfyUIContextManager: 247 | def __init__(self, *args, **kwargs): 248 | self.client = ComfyUIClient(*args, **kwargs) 249 | 250 | def __enter__(self): 251 | self.client.connect() 252 | return self.client 253 | 254 | def __exit__(self, exc_type, exc_val, exc_tb): 255 | self.client.close() 256 | 257 | # 使用法 258 | with ComfyUIContextManager("localhost:8188", "workflow.json") as client: 259 | client.set_data(key='KSampler', seed=12345) 260 | results = client.generate(["Result Image"]) 261 | ``` 262 | 263 | ### バッチ処理 264 | 265 | ```python 266 | import random 267 | 268 | prompts = ["山に沈む夕日", "夜の街", "森の湖"] 269 | seeds = [random.randint(0, 2**32) for _ in range(3)] 270 | 271 | client = ComfyUIClient("localhost:8188", "workflow.json") 272 | client.connect() 273 | 274 | for i, (prompt, seed) in enumerate(zip(prompts, seeds)): 275 | client.set_data(key='CLIP Text Encode Positive', text=prompt) 276 | client.set_data(key='KSampler', seed=seed) 277 | 278 | results = client.generate(["Result Image"]) 279 | for key, image in results.items(): 280 | image.save(f"output_{i}_{key}.png") 281 | 282 | client.close() 283 | ``` 284 | 285 | ### 動的ワークフロー更新 286 | 287 | ```python 288 | client = ComfyUIClient("localhost:8188", "workflow.json") 289 | client.connect() 290 | 291 | # 初回生成 292 | client.set_data(key='KSampler', seed=12345) 293 | results = client.generate(["Result Image"]) 294 | 295 | # 外部でワークフローファイルを変更した後、リロード 296 | client.reload() 297 | 298 | # 更新されたワークフローを使用 299 | client.set_data(key='KSampler', seed=67890) 300 | results = client.generate(["Result Image"]) 301 | 302 | client.close() 303 | ``` 304 | 305 | ## テスト 306 | 307 | テストスイートを実行: 308 | 309 | ```bash 310 | # 基本機能テスト 311 | python test_workflow_loading.py 312 | 313 | # エラーハンドリングテスト 314 | python test_error_handling.py 315 | 316 | # 拡張機能テスト 317 | python test_enhanced_features.py 318 | 319 | # フォーマット変換テスト 320 | python test_conversion.py 321 | ``` 322 | 323 | ## トラブルシューティング 324 | 325 | ### よくある問題 326 | 327 | **1. 接続拒否** 328 | ``` 329 | ConnectionError: Failed to connect to ComfyUI server 330 | ``` 331 | - 指定されたアドレスでComfyUIが実行されていることを確認 332 | - ファイアウォール設定を確認 333 | - ポート番号を確認 334 | 335 | **2. キーが見つからない** 336 | ``` 337 | Key not found: NodeName 338 | ``` 339 | - ComfyUIインターフェースでノードタイトルを確認 340 | - タイトルの代わりにclass_typeを試す 341 | - デバッグモードを有効にして利用可能なノードを確認 342 | 343 | **3. タイムアウトエラー** 344 | ``` 345 | TimeoutError: Timeout waiting for prompt to complete 346 | ``` 347 | - 複雑なワークフローは5分以上かかる場合がある 348 | - ComfyUIサーバーのパフォーマンスを確認 349 | - ワークフローが有効であることを確認 350 | 351 | ### デバッグのコツ 352 | 353 | 1. **デバッグモードを有効にする**詳細ログを取得: 354 | ```python 355 | client = ComfyUIClient("localhost:8188", "workflow.json", debug=True) 356 | ``` 357 | 358 | 2. **ワークフロー内のノード名を確認**: 359 | ```python 360 | client = ComfyUIClient("localhost:8188", "workflow.json", debug=True) 361 | # デバッグ出力で利用可能なノードIDとタイトルが表示される 362 | ``` 363 | 364 | 3. **クライアントを使用する前にComfyUIでワークフローをテスト** 365 | 366 | 4. **フォーマット変換を使用してワークフローを理解**: 367 | ```python 368 | api_format = convert_workflow_to_api("workflow.json") 369 | print(json.dumps(api_format, indent=2)) 370 | ``` 371 | 372 | ## 実用的な使用例 373 | 374 | ### AI画像生成パイプライン 375 | 376 | ```python 377 | import random 378 | from comfyuiclient import ComfyUIClient 379 | 380 | def generate_artwork(prompt, style="realistic", steps=20): 381 | """AI アートワーク生成関数""" 382 | client = ComfyUIClient("localhost:8188", "workflow.json") 383 | client.connect() 384 | 385 | try: 386 | # プロンプトとスタイルを設定 387 | full_prompt = f"{prompt}, {style} style" 388 | client.set_data(key='CLIP Text Encode Positive', text=full_prompt) 389 | client.set_data(key='KSampler', seed=random.randint(0, 2**32)) 390 | client.set_data(key='KSampler', input_key='steps', input_value=steps) 391 | 392 | # 画像生成 393 | results = client.generate(["Result Image"]) 394 | 395 | return list(results.values())[0] # 最初の画像を返す 396 | finally: 397 | client.close() 398 | 399 | # 使用例 400 | image = generate_artwork("桜咲く日本庭園", "anime", 25) 401 | image.save("japanese_garden.png") 402 | ``` 403 | 404 | ### バッチ画像生成システム 405 | 406 | ```python 407 | import asyncio 408 | from comfyuiclient import ComfyUIClientAsync 409 | 410 | async def batch_generate(prompts, output_dir="outputs"): 411 | """複数プロンプトの一括生成""" 412 | import os 413 | os.makedirs(output_dir, exist_ok=True) 414 | 415 | client = ComfyUIClientAsync("localhost:8188", "workflow.json") 416 | await client.connect() 417 | 418 | try: 419 | for i, prompt in enumerate(prompts): 420 | print(f"生成中 {i+1}/{len(prompts)}: {prompt}") 421 | 422 | await client.set_data(key='CLIP Text Encode Positive', text=prompt) 423 | await client.set_data(key='KSampler', seed=i * 1000) 424 | 425 | results = await client.generate(["Result Image"]) 426 | 427 | for key, image in results.items(): 428 | filename = f"{output_dir}/image_{i:03d}_{key}.png" 429 | image.save(filename) 430 | print(f"保存完了: {filename}") 431 | finally: 432 | await client.close() 433 | 434 | # 使用例 435 | prompts = [ 436 | "美しい夕焼けの海岸", 437 | "雪山の頂上から見る景色", 438 | "都市の夜景", 439 | "森の中の小さな家" 440 | ] 441 | 442 | asyncio.run(batch_generate(prompts)) 443 | ``` 444 | 445 | ### 画像バリエーション生成 446 | 447 | ```python 448 | def create_variations(base_prompt, variations, count=4): 449 | """ベースプロンプトから複数のバリエーションを生成""" 450 | client = ComfyUIClient("localhost:8188", "workflow.json") 451 | client.connect() 452 | 453 | all_images = [] 454 | 455 | try: 456 | for var in variations: 457 | for i in range(count): 458 | prompt = f"{base_prompt}, {var}" 459 | 460 | client.set_data(key='CLIP Text Encode Positive', text=prompt) 461 | client.set_data(key='KSampler', seed=random.randint(0, 2**32)) 462 | 463 | results = client.generate(["Result Image"]) 464 | 465 | for key, image in results.items(): 466 | filename = f"{var}_{i+1}.png" 467 | image.save(filename) 468 | all_images.append((filename, image)) 469 | 470 | finally: 471 | client.close() 472 | 473 | return all_images 474 | 475 | # 使用例 476 | base = "ファンタジーの風景" 477 | variations = ["朝の光", "夕暮れ", "星空", "雨の日"] 478 | images = create_variations(base, variations, 2) 479 | 480 | print(f"合計 {len(images)} 枚の画像を生成しました") 481 | ``` 482 | 483 | ## ライセンス 484 | 485 | このプロジェクトはMITライセンスのもとでライセンスされています - 詳細はLICENSEファイルを参照してください。 486 | 487 | ## 貢献 488 | 489 | 1. リポジトリをフォーク 490 | 2. フィーチャーブランチを作成 491 | 3. 新機能にテストを追加 492 | 4. すべてのテストが通ることを確認 493 | 5. プルリクエストを提出 494 | 495 | ## 変更履歴 496 | 497 | ### 最新バージョン 498 | - ✅ 特定の例外タイプを持つ拡張エラーハンドリング 499 | - ✅ 開発とトラブルシューティング用のデバッグモード 500 | - ✅ workflow.jsonからAPIフォーマットへの自動変換 501 | - ✅ 動的ワークフローリロード 502 | - ✅ 任意パラメータサポートを持つ拡張set_data() 503 | - ✅ タイトルまたはclass_typeによるスマートノード検索 504 | - ✅ 包括的テストスイート 505 | - ✅ 長時間実行操作のタイムアウト処理 506 | - ✅ 堅牢なリソースクリーンアップ 507 | 508 | ## サポート 509 | 510 | 質問や問題がある場合は、GitHubのIssuesページで報告してください。 511 | 512 | ## 関連リンク 513 | 514 | - [ComfyUI公式リポジトリ](https://github.com/comfyanonymous/ComfyUI) 515 | - [ComfyUI API ドキュメント](https://docs.comfy.org/essentials/comfyui_api) 516 | - [Python Pillow ドキュメント](https://pillow.readthedocs.io/) -------------------------------------------------------------------------------- /tests/test_comfyui_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Comprehensive test script for ComfyUIClient 4 | Tests both sync and async versions with both workflow formats 5 | """ 6 | 7 | import asyncio 8 | import json 9 | import sys 10 | import os 11 | import random 12 | import traceback 13 | from pathlib import Path 14 | from comfyuiclient import ComfyUIClient, ComfyUIClientAsync, convert_workflow_to_api 15 | 16 | # Configuration 17 | SERVER_ADDRESS = "192.168.1.27:8188" # Update this to your ComfyUI server address 18 | TEST_PROMPT = "a beautiful sunset over mountains, digital art" 19 | TEST_NEGATIVE_PROMPT = "low quality, blurry" 20 | 21 | # ANSI color codes for output 22 | class Colors: 23 | HEADER = '\033[95m' 24 | BLUE = '\033[94m' 25 | CYAN = '\033[96m' 26 | GREEN = '\033[92m' 27 | WARNING = '\033[93m' 28 | FAIL = '\033[91m' 29 | ENDC = '\033[0m' 30 | BOLD = '\033[1m' 31 | UNDERLINE = '\033[4m' 32 | 33 | 34 | def print_header(text): 35 | """Print a formatted header""" 36 | print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}") 37 | print(f"{Colors.HEADER}{Colors.BOLD}{text.center(60)}{Colors.ENDC}") 38 | print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}\n") 39 | 40 | 41 | def print_test_case(test_name): 42 | """Print a test case header""" 43 | print(f"{Colors.CYAN}{Colors.BOLD}Testing: {test_name}{Colors.ENDC}") 44 | print(f"{Colors.CYAN}{'-'*40}{Colors.ENDC}") 45 | 46 | 47 | def print_success(message): 48 | """Print success message""" 49 | print(f"{Colors.GREEN}✓ {message}{Colors.ENDC}") 50 | 51 | 52 | def print_error(message): 53 | """Print error message""" 54 | print(f"{Colors.FAIL}✗ {message}{Colors.ENDC}") 55 | 56 | 57 | def print_info(message): 58 | """Print info message""" 59 | print(f"{Colors.BLUE}ℹ {message}{Colors.ENDC}") 60 | 61 | 62 | def verify_workflow_format(workflow_file): 63 | """Verify and display workflow format type""" 64 | with open(workflow_file, 'r', encoding='utf8') as f: 65 | data = json.load(f) 66 | 67 | if 'nodes' in data and 'links' in data: 68 | print_info(f"{workflow_file} is in ComfyUI workflow format (needs conversion)") 69 | print_info(f" - Contains {len(data.get('nodes', []))} nodes") 70 | print_info(f" - Contains {len(data.get('links', []))} links") 71 | return "workflow" 72 | else: 73 | print_info(f"{workflow_file} is in API format (ready to use)") 74 | print_info(f" - Contains {len(data)} node definitions") 75 | return "api" 76 | 77 | 78 | def test_format_conversion(): 79 | """Test the workflow format conversion function""" 80 | print_test_case("Format Conversion Function") 81 | 82 | try: 83 | # Test converting workflow.json 84 | with open('workflow.json', 'r', encoding='utf8') as f: 85 | workflow_data = json.load(f) 86 | 87 | api_format = convert_workflow_to_api(workflow_data) 88 | 89 | print_success("Successfully converted workflow.json to API format") 90 | print_info(f" - Original nodes: {len(workflow_data.get('nodes', []))}") 91 | print_info(f" - Converted nodes: {len(api_format)}") 92 | 93 | # Verify structure 94 | for node_id, node_data in api_format.items(): 95 | if 'class_type' not in node_data: 96 | print_error(f"Node {node_id} missing class_type") 97 | return False 98 | if 'inputs' not in node_data: 99 | print_error(f"Node {node_id} missing inputs") 100 | return False 101 | 102 | print_success("All converted nodes have correct structure") 103 | 104 | # Compare with workflow_api.json 105 | with open('workflow_api.json', 'r', encoding='utf8') as f: 106 | reference_api = json.load(f) 107 | 108 | # Check if key nodes exist 109 | key_nodes_found = 0 110 | for node_id in reference_api: 111 | if node_id in api_format: 112 | key_nodes_found += 1 113 | 114 | print_info(f" - Matching nodes with reference: {key_nodes_found}/{len(reference_api)}") 115 | 116 | return True 117 | 118 | except Exception as e: 119 | print_error(f"Format conversion failed: {str(e)}") 120 | traceback.print_exc() 121 | return False 122 | 123 | 124 | def test_sync_client(workflow_file): 125 | """Test synchronous ComfyUIClient""" 126 | print_test_case(f"Sync Client with {workflow_file}") 127 | 128 | client = None 129 | try: 130 | # Verify format before testing 131 | format_type = verify_workflow_format(workflow_file) 132 | 133 | # Initialize client 134 | client = ComfyUIClient(SERVER_ADDRESS, workflow_file) 135 | print_success("Client initialized successfully") 136 | 137 | # Test automatic format detection 138 | print_info("Testing automatic format detection...") 139 | if format_type == "workflow" and 'nodes' not in client.comfyui_prompt: 140 | print_success("Workflow format was automatically converted to API format") 141 | elif format_type == "api" and 'nodes' not in client.comfyui_prompt: 142 | print_success("API format was loaded directly without conversion") 143 | 144 | # Connect to server 145 | client.connect() 146 | print_success("Connected to ComfyUI server") 147 | 148 | # Set test data 149 | test_seed = random.randint(0, sys.maxsize) 150 | client.set_data(key='KSampler', seed=test_seed) 151 | print_success(f"Set random seed: {test_seed}") 152 | 153 | # Try different possible node names for positive prompt 154 | positive_set = False 155 | for node_name in ['CLIP Text Encode Positive', 'CLIPTextEncode', 'positive']: 156 | try: 157 | client.set_data(key=node_name, text=TEST_PROMPT) 158 | print_success(f"Set positive prompt using node: {node_name}") 159 | positive_set = True 160 | break 161 | except: 162 | continue 163 | 164 | if not positive_set: 165 | print_warning("Could not find positive prompt node") 166 | 167 | # Generate image 168 | print_info("Generating image...") 169 | output_filename = f"test_sync_{Path(workflow_file).stem}.png" 170 | 171 | # Try different possible output node names 172 | for node_name in ['Result Image', 'SaveImage', 'PreviewImage']: 173 | try: 174 | results = client.generate([node_name]) 175 | if results: 176 | for key, image in results.items(): 177 | image.save(output_filename) 178 | print_success(f"Generated and saved image: {output_filename}") 179 | print_info(f" - Image size: {image.size}") 180 | print_info(f" - Image mode: {image.mode}") 181 | break 182 | except: 183 | continue 184 | 185 | return True 186 | 187 | except Exception as e: 188 | print_error(f"Sync client test failed: {str(e)}") 189 | traceback.print_exc() 190 | return False 191 | finally: 192 | if client: 193 | client.close() 194 | print_info("Client connection closed") 195 | 196 | 197 | async def test_async_client(workflow_file): 198 | """Test asynchronous ComfyUIClient""" 199 | print_test_case(f"Async Client with {workflow_file}") 200 | 201 | client = None 202 | try: 203 | # Verify format before testing 204 | format_type = verify_workflow_format(workflow_file) 205 | 206 | # Initialize client 207 | client = ComfyUIClientAsync(SERVER_ADDRESS, workflow_file) 208 | print_success("Async client initialized successfully") 209 | 210 | # Test automatic format detection 211 | print_info("Testing automatic format detection...") 212 | if format_type == "workflow" and 'nodes' not in client.comfyui_prompt: 213 | print_success("Workflow format was automatically converted to API format") 214 | elif format_type == "api" and 'nodes' not in client.comfyui_prompt: 215 | print_success("API format was loaded directly without conversion") 216 | 217 | # Connect to server 218 | await client.connect() 219 | print_success("Connected to ComfyUI server (async)") 220 | 221 | # Set test data 222 | test_seed = random.randint(0, sys.maxsize) 223 | await client.set_data(key='KSampler', seed=test_seed) 224 | print_success(f"Set random seed: {test_seed}") 225 | 226 | # Try different possible node names for positive prompt 227 | positive_set = False 228 | for node_name in ['CLIP Text Encode Positive', 'CLIPTextEncode', 'positive']: 229 | try: 230 | await client.set_data(key=node_name, text=TEST_PROMPT) 231 | print_success(f"Set positive prompt using node: {node_name}") 232 | positive_set = True 233 | break 234 | except: 235 | continue 236 | 237 | if not positive_set: 238 | print_warning("Could not find positive prompt node") 239 | 240 | # Generate image 241 | print_info("Generating image...") 242 | output_filename = f"test_async_{Path(workflow_file).stem}.png" 243 | 244 | # Try different possible output node names 245 | for node_name in ['Result Image', 'SaveImage', 'PreviewImage']: 246 | try: 247 | results = await client.generate([node_name]) 248 | if results: 249 | for key, image in results.items(): 250 | image.save(output_filename) 251 | print_success(f"Generated and saved image: {output_filename}") 252 | print_info(f" - Image size: {image.size}") 253 | print_info(f" - Image mode: {image.mode}") 254 | break 255 | except: 256 | continue 257 | 258 | return True 259 | 260 | except Exception as e: 261 | print_error(f"Async client test failed: {str(e)}") 262 | traceback.print_exc() 263 | return False 264 | finally: 265 | if client: 266 | await client.close() 267 | print_info("Async client connection closed") 268 | 269 | 270 | def print_warning(message): 271 | """Print warning message""" 272 | print(f"{Colors.WARNING}⚠ {message}{Colors.ENDC}") 273 | 274 | 275 | async def run_all_tests(): 276 | """Run all test combinations""" 277 | print_header("ComfyUI Client Comprehensive Test Suite") 278 | 279 | # Check if server is reachable 280 | print_test_case("Server Connection Check") 281 | try: 282 | import requests 283 | response = requests.get(f"http://{SERVER_ADDRESS}/system_stats", timeout=5) 284 | if response.status_code == 200: 285 | print_success(f"ComfyUI server is reachable at {SERVER_ADDRESS}") 286 | else: 287 | print_warning(f"Server responded with status code: {response.status_code}") 288 | except Exception as e: 289 | print_error(f"Cannot reach ComfyUI server at {SERVER_ADDRESS}") 290 | print_error("Please ensure ComfyUI is running and update SERVER_ADDRESS in this script") 291 | return 292 | 293 | # Test format conversion 294 | print_header("Testing Format Conversion") 295 | conversion_success = test_format_conversion() 296 | 297 | # Test all combinations 298 | test_results = { 299 | "Format Conversion": conversion_success, 300 | "Sync + workflow.json": False, 301 | "Sync + workflow_api.json": False, 302 | "Async + workflow.json": False, 303 | "Async + workflow_api.json": False 304 | } 305 | 306 | # Run sync tests 307 | print_header("Synchronous Client Tests") 308 | 309 | if os.path.exists("workflow.json"): 310 | test_results["Sync + workflow.json"] = test_sync_client("workflow.json") 311 | else: 312 | print_warning("workflow.json not found, skipping test") 313 | 314 | if os.path.exists("workflow_api.json"): 315 | test_results["Sync + workflow_api.json"] = test_sync_client("workflow_api.json") 316 | else: 317 | print_warning("workflow_api.json not found, skipping test") 318 | 319 | # Run async tests 320 | print_header("Asynchronous Client Tests") 321 | 322 | if os.path.exists("workflow.json"): 323 | test_results["Async + workflow.json"] = await test_async_client("workflow.json") 324 | else: 325 | print_warning("workflow.json not found, skipping test") 326 | 327 | if os.path.exists("workflow_api.json"): 328 | test_results["Async + workflow_api.json"] = await test_async_client("workflow_api.json") 329 | else: 330 | print_warning("workflow_api.json not found, skipping test") 331 | 332 | # Print summary 333 | print_header("Test Summary") 334 | 335 | total_tests = len(test_results) 336 | passed_tests = sum(1 for result in test_results.values() if result) 337 | 338 | print(f"{Colors.BOLD}Total Tests: {total_tests}{Colors.ENDC}") 339 | print(f"{Colors.GREEN}{Colors.BOLD}Passed: {passed_tests}{Colors.ENDC}") 340 | print(f"{Colors.FAIL}{Colors.BOLD}Failed: {total_tests - passed_tests}{Colors.ENDC}") 341 | print() 342 | 343 | for test_name, result in test_results.items(): 344 | status = f"{Colors.GREEN}PASS{Colors.ENDC}" if result else f"{Colors.FAIL}FAIL{Colors.ENDC}" 345 | print(f" {test_name:<30} [{status}]") 346 | 347 | print() 348 | if passed_tests == total_tests: 349 | print(f"{Colors.GREEN}{Colors.BOLD}All tests passed! 🎉{Colors.ENDC}") 350 | else: 351 | print(f"{Colors.WARNING}{Colors.BOLD}Some tests failed. Please check the output above.{Colors.ENDC}") 352 | 353 | # List generated files 354 | print_header("Generated Files") 355 | generated_files = [ 356 | "test_sync_workflow.png", 357 | "test_sync_workflow_api.png", 358 | "test_async_workflow.png", 359 | "test_async_workflow_api.png" 360 | ] 361 | 362 | for file in generated_files: 363 | if os.path.exists(file): 364 | size = os.path.getsize(file) 365 | print_success(f"{file} ({size:,} bytes)") 366 | else: 367 | print_info(f"{file} (not generated)") 368 | 369 | 370 | def main(): 371 | """Main entry point""" 372 | # Run the async test suite 373 | asyncio.run(run_all_tests()) 374 | 375 | 376 | if __name__ == "__main__": 377 | main() -------------------------------------------------------------------------------- /comfyuiclient/client.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import random 4 | import sys 5 | import time 6 | import uuid 7 | 8 | import aiohttp 9 | import requests 10 | from PIL import Image 11 | 12 | 13 | def convert_workflow_to_api(workflow_json): 14 | """ 15 | Convert ComfyUI workflow format to API format. 16 | 17 | Args: 18 | workflow_json: Dict or path to workflow.json file 19 | 20 | Returns: 21 | API format dict ready for ComfyUI API 22 | """ 23 | # Load from file if path is provided 24 | if isinstance(workflow_json, str): 25 | with open(workflow_json, "r", encoding="utf8") as f: 26 | workflow_json = json.load(f) 27 | 28 | api_json = {} 29 | 30 | # Create link lookup table 31 | link_map = {} 32 | for link in workflow_json.get("links", []): 33 | # link format: [link_id, source_node, source_slot, target_node, target_slot, type] 34 | link_id = link[0] 35 | source_node = link[1] 36 | source_slot = link[2] 37 | link_map[link_id] = [str(source_node), source_slot] 38 | 39 | # Widget value mappings for different node types 40 | widget_mappings = { 41 | "KSampler": [ 42 | "seed", 43 | "seed_control", 44 | "steps", 45 | "cfg", 46 | "sampler_name", 47 | "scheduler", 48 | "denoise", 49 | ], 50 | "CLIPTextEncode": ["text"], 51 | "EmptyLatentImage": ["width", "height", "batch_size"], 52 | "CheckpointLoaderSimple": ["ckpt_name"], 53 | "SaveImage": ["filename_prefix"], 54 | "PreviewImage": [], 55 | "VAEDecode": [], 56 | "VAEEncode": [], 57 | "VAELoader": ["vae_name"], 58 | "LoraLoader": ["lora_name", "strength_model", "strength_clip"], 59 | "ControlNetLoader": ["control_net_name"], 60 | "LoadImage": ["image", "upload"], 61 | "ImageScale": ["upscale_method", "width", "height", "crop"], 62 | } 63 | 64 | # Process each node 65 | for node in workflow_json.get("nodes", []): 66 | node_id = str(node["id"]) 67 | node_type = node["type"] 68 | 69 | api_node = { 70 | "class_type": node_type, 71 | "_meta": {"title": node.get("title", node_type)}, 72 | } 73 | 74 | inputs = {} 75 | 76 | # Map widget values to named inputs 77 | widget_values = node.get("widgets_values", []) 78 | if node_type in widget_mappings: 79 | param_names = widget_mappings[node_type] 80 | for i, param_name in enumerate(param_names): 81 | if i < len(widget_values): 82 | # Skip "randomize" value for seed_control in KSampler 83 | if param_name == "seed_control" and widget_values[i] == "randomize": 84 | continue 85 | inputs[param_name] = widget_values[i] 86 | 87 | # Add connected inputs 88 | for input_def in node.get("inputs", []): 89 | if "link" in input_def and input_def["link"] is not None: 90 | input_name = input_def["name"].lower().replace(" ", "_") 91 | inputs[input_name] = link_map.get(input_def["link"]) 92 | 93 | api_node["inputs"] = inputs 94 | api_json[node_id] = api_node 95 | 96 | return api_json 97 | 98 | 99 | class ComfyUIClientAsync: 100 | 101 | def __init__(self, server, prompt_file, debug=False): 102 | self.PROMPT_FILE = prompt_file 103 | self.SERVER_ADDRESS = server 104 | self.CLIENT_ID = str(uuid.uuid4()) 105 | self.ws = None 106 | self.session = None 107 | self.debug = debug 108 | 109 | self.reload() 110 | 111 | def reload(self): 112 | """Reload workflow file and convert if needed""" 113 | try: 114 | with open(self.PROMPT_FILE, "r", encoding="utf8") as f: 115 | data = json.load(f) 116 | 117 | # Convert workflow.json to API format if needed 118 | if "nodes" in data and "links" in data: 119 | self.comfyui_prompt = convert_workflow_to_api(data) 120 | else: 121 | self.comfyui_prompt = data 122 | 123 | if self.debug: 124 | print(f"Loaded workflow from {self.PROMPT_FILE}") 125 | except FileNotFoundError: 126 | print(f"Prompt file not found: {self.PROMPT_FILE}") 127 | except json.JSONDecodeError: 128 | print(f"Failed to parse prompt file: {self.PROMPT_FILE}") 129 | except Exception as e: 130 | print(f"Error: {e} while reading prompt file: {self.PROMPT_FILE}") 131 | 132 | async def connect(self): 133 | try: 134 | self.session = aiohttp.ClientSession() 135 | self.ws = await self.session.ws_connect( 136 | f"ws://{self.SERVER_ADDRESS}/ws?clientId={self.CLIENT_ID}" 137 | ) 138 | except aiohttp.ClientError as e: 139 | if self.session: 140 | await self.session.close() 141 | raise ConnectionError(f"Failed to connect to ComfyUI server: {e}") 142 | 143 | async def close(self): 144 | try: 145 | if self.ws: 146 | await self.ws.close() 147 | except Exception as e: 148 | if self.debug: 149 | print(f"Error closing WebSocket: {e}") 150 | try: 151 | if self.session: 152 | await self.session.close() 153 | except Exception as e: 154 | if self.debug: 155 | print(f"Error closing session: {e}") 156 | 157 | async def queue_prompt(self, prompt): 158 | try: 159 | payload = {"prompt": prompt, "client_id": self.CLIENT_ID} 160 | async with self.session.post( 161 | f"http://{self.SERVER_ADDRESS}/prompt", json=payload 162 | ) as response: 163 | response.raise_for_status() 164 | result = await response.json() 165 | if "prompt_id" not in result: 166 | raise ValueError("Server response missing prompt_id") 167 | return result 168 | except aiohttp.ClientError as e: 169 | raise ConnectionError(f"Failed to queue prompt: {e}") 170 | except json.JSONDecodeError as e: 171 | raise ValueError(f"Invalid JSON response from server: {e}") 172 | 173 | async def get_image(self, filename, subfolder, folder_type): 174 | try: 175 | params = {"filename": filename, "subfolder": subfolder, "type": folder_type} 176 | async with self.session.get( 177 | f"http://{self.SERVER_ADDRESS}/view", params=params 178 | ) as response: 179 | response.raise_for_status() 180 | return await response.read() 181 | except aiohttp.ClientError as e: 182 | raise ConnectionError(f"Failed to get image {filename}: {e}") 183 | 184 | async def get_history(self, prompt_id): 185 | try: 186 | async with self.session.get( 187 | f"http://{self.SERVER_ADDRESS}/history/{prompt_id}" 188 | ) as response: 189 | response.raise_for_status() 190 | return await response.json() 191 | except aiohttp.ClientError as e: 192 | raise ConnectionError(f"Failed to get history for {prompt_id}: {e}") 193 | except json.JSONDecodeError as e: 194 | raise ValueError(f"Invalid JSON response from server: {e}") 195 | 196 | async def get_images(self, prompt): 197 | prompt_id = (await self.queue_prompt(prompt))["prompt_id"] 198 | output_images = {} 199 | output_text = {} 200 | 201 | while True: 202 | message = await self.ws.receive() 203 | if message.type == aiohttp.WSMsgType.TEXT: 204 | data = json.loads(message.data) 205 | if ( 206 | data["type"] == "executing" 207 | and data["data"]["node"] is None 208 | and data["data"]["prompt_id"] == prompt_id 209 | ): 210 | break 211 | 212 | history = (await self.get_history(prompt_id))[prompt_id] 213 | for node_id, node_output in history["outputs"].items(): 214 | images_output = [] 215 | if "images" in node_output: 216 | for image in node_output["images"]: 217 | image_data = await self.get_image( 218 | image["filename"], image["subfolder"], image["type"] 219 | ) 220 | images_output.append(image_data) 221 | output_images[node_id] = images_output 222 | if "text" in node_output: 223 | output_text[node_id] = node_output["text"] 224 | 225 | return output_images, output_text 226 | 227 | async def set_data( 228 | self, 229 | key, 230 | text: str = None, 231 | seed: int = None, 232 | image: Image.Image = None, 233 | number: float = None, 234 | value: float = None, 235 | input_key: str = None, 236 | input_value=None, 237 | ): 238 | key_id = self.find_key_by_title(key) 239 | if key_id is None: 240 | return 241 | 242 | if input_key is not None and input_value is not None: 243 | self.comfyui_prompt[key_id]["inputs"][input_key] = input_value 244 | if text is not None: 245 | self.comfyui_prompt[key_id]["inputs"]["text"] = text 246 | if seed is not None: 247 | self.comfyui_prompt[key_id]["inputs"]["seed"] = int(seed) 248 | if number is not None: 249 | self.comfyui_prompt[key_id]["inputs"]["Number"] = number 250 | if value is not None: 251 | self.comfyui_prompt[key_id]["inputs"]["value"] = value 252 | if image is not None: 253 | try: 254 | # Upload image to comfyui server 255 | folder_name = "temp" 256 | 257 | # Save image to byte data 258 | byte_data = io.BytesIO() 259 | image.save(byte_data, format="PNG") 260 | byte_data.seek(0) 261 | 262 | # Upload image using existing session 263 | data = aiohttp.FormData() 264 | data.add_field("image", byte_data, filename="temp.png") 265 | data.add_field("subfolder", folder_name) 266 | 267 | async with self.session.post( 268 | f"http://{self.SERVER_ADDRESS}/upload/image", data=data 269 | ) as response: 270 | response.raise_for_status() 271 | resp_json = await response.json() 272 | 273 | if "name" not in resp_json or "subfolder" not in resp_json: 274 | raise ValueError( 275 | "Invalid upload response: missing required fields" 276 | ) 277 | 278 | # Set image path 279 | self.comfyui_prompt[key_id]["inputs"]["image"] = ( 280 | resp_json.get("subfolder") + "/" + resp_json.get("name") 281 | ) 282 | except aiohttp.ClientError as e: 283 | raise ConnectionError(f"Failed to upload image: {e}") 284 | except Exception as e: 285 | raise RuntimeError(f"Error processing image upload: {e}") 286 | 287 | if self.debug: 288 | print(f"Set data for {key} (id: {key_id}): {self.comfyui_prompt[key_id]}") 289 | 290 | def find_key_by_title(self, target_title): 291 | target_title = target_title.strip() 292 | for key, value in self.comfyui_prompt.items(): 293 | # Check class_type first 294 | class_type = value.get("class_type", "").strip() 295 | if class_type == target_title: 296 | return key 297 | # Then check title 298 | title = value.get("_meta", {}).get("title", "").strip() 299 | if title == target_title: 300 | return key 301 | if self.debug: 302 | print(f"Key not found: {target_title}") 303 | return None 304 | 305 | async def generate(self, node_names=None) -> dict: 306 | node_ids = {} 307 | if node_names is not None: 308 | for node_name in node_names: 309 | node_id = self.find_key_by_title(node_name) 310 | if node_id is not None: 311 | node_ids[node_id] = node_name 312 | 313 | images, text = await self.get_images(self.comfyui_prompt) 314 | results = {} 315 | for node_id, node_images in images.items(): 316 | if node_id in node_ids: 317 | for image_data in node_images: 318 | image = Image.open(io.BytesIO(image_data)) 319 | results[node_ids[node_id]] = image 320 | for node_id, node_text in text.items(): 321 | if node_id in node_ids: 322 | results[node_ids[node_id]] = node_text 323 | 324 | return results 325 | 326 | 327 | class ComfyUIClient: 328 | 329 | def __init__(self, server, prompt_file, debug=False): 330 | self.PROMPT_FILE = prompt_file 331 | self.SERVER_ADDRESS = server 332 | self.CLIENT_ID = str(uuid.uuid4()) 333 | self.session = None 334 | self.debug = debug 335 | 336 | self.reload() 337 | 338 | def reload(self): 339 | """Reload workflow file and convert if needed""" 340 | try: 341 | with open(self.PROMPT_FILE, "r", encoding="utf8") as f: 342 | data = json.load(f) 343 | 344 | # Convert workflow.json to API format if needed 345 | if "nodes" in data and "links" in data: 346 | self.comfyui_prompt = convert_workflow_to_api(data) 347 | else: 348 | self.comfyui_prompt = data 349 | 350 | if self.debug: 351 | print(f"Loaded workflow from {self.PROMPT_FILE}") 352 | except FileNotFoundError: 353 | print(f"Prompt file not found: {self.PROMPT_FILE}") 354 | except json.JSONDecodeError: 355 | print(f"Failed to parse prompt file: {self.PROMPT_FILE}") 356 | except Exception as e: 357 | print(f"Error: {e} while reading prompt file: {self.PROMPT_FILE}") 358 | 359 | def connect(self): 360 | self.session = requests.Session() 361 | 362 | def close(self): 363 | if self.session is not None: 364 | self.session.close() 365 | self.session = None 366 | 367 | def queue_prompt(self, prompt): 368 | try: 369 | payload = {"prompt": prompt, "client_id": self.CLIENT_ID} 370 | response = self.session.post( 371 | f"http://{self.SERVER_ADDRESS}/prompt", json=payload 372 | ) 373 | response.raise_for_status() 374 | result = response.json() 375 | if "prompt_id" not in result: 376 | raise ValueError("Server response missing prompt_id") 377 | return result 378 | except requests.RequestException as e: 379 | raise ConnectionError(f"Failed to queue prompt: {e}") 380 | except json.JSONDecodeError as e: 381 | raise ValueError(f"Invalid JSON response from server: {e}") 382 | 383 | def get_image(self, filename, subfolder, folder_type): 384 | try: 385 | params = {"filename": filename, "subfolder": subfolder, "type": folder_type} 386 | response = self.session.get( 387 | f"http://{self.SERVER_ADDRESS}/view", params=params 388 | ) 389 | response.raise_for_status() 390 | return response.content 391 | except requests.RequestException as e: 392 | raise ConnectionError(f"Failed to get image {filename}: {e}") 393 | 394 | def get_history(self, prompt_id): 395 | try: 396 | response = self.session.get( 397 | f"http://{self.SERVER_ADDRESS}/history/{prompt_id}" 398 | ) 399 | response.raise_for_status() 400 | return response.json() 401 | except requests.RequestException as e: 402 | raise ConnectionError(f"Failed to get history for {prompt_id}: {e}") 403 | except json.JSONDecodeError as e: 404 | raise ValueError(f"Invalid JSON response from server: {e}") 405 | 406 | def get_images(self, prompt): 407 | result = self.queue_prompt(prompt) 408 | prompt_id = result.get("prompt_id") 409 | if not prompt_id: 410 | raise ValueError("Failed to get prompt_id from server response") 411 | 412 | output_images = {} 413 | output_text = {} 414 | max_retries = 300 # Maximum 5 minutes wait 415 | retry_count = 0 416 | 417 | while retry_count < max_retries: 418 | try: 419 | history = self.get_history(prompt_id) 420 | if prompt_id in history and "outputs" in history[prompt_id]: 421 | break 422 | time.sleep(1) 423 | retry_count += 1 424 | except Exception as e: 425 | if self.debug: 426 | print(f"Error getting history (retry {retry_count}): {e}") 427 | if retry_count >= max_retries: 428 | raise TimeoutError( 429 | f"Timeout waiting for prompt {prompt_id} to complete" 430 | ) 431 | time.sleep(1) 432 | retry_count += 1 433 | 434 | if retry_count >= max_retries: 435 | raise TimeoutError(f"Timeout waiting for prompt {prompt_id} to complete") 436 | 437 | for node_id, node_output in history[prompt_id]["outputs"].items(): 438 | images_output = [] 439 | if "images" in node_output: 440 | for image in node_output["images"]: 441 | image_data = self.get_image( 442 | image["filename"], image["subfolder"], image["type"] 443 | ) 444 | images_output.append(image_data) 445 | output_images[node_id] = images_output 446 | if "text" in node_output: 447 | output_text[node_id] = node_output["text"] 448 | 449 | return output_images, output_text 450 | 451 | def set_data( 452 | self, 453 | key, 454 | text: str = None, 455 | seed: int = None, 456 | image: Image.Image = None, 457 | number: float = None, 458 | value: float = None, 459 | input_key: str = None, 460 | input_value=None, 461 | ): 462 | key_id = self.find_key_by_title(key) 463 | if key_id is None: 464 | return 465 | 466 | if input_key is not None and input_value is not None: 467 | self.comfyui_prompt[key_id]["inputs"][input_key] = input_value 468 | if text is not None: 469 | self.comfyui_prompt[key_id]["inputs"]["text"] = text 470 | if seed is not None: 471 | self.comfyui_prompt[key_id]["inputs"]["seed"] = int(seed) 472 | if number is not None: 473 | self.comfyui_prompt[key_id]["inputs"]["Number"] = number 474 | if value is not None: 475 | self.comfyui_prompt[key_id]["inputs"]["value"] = value 476 | if image is not None: 477 | # Upload image to comfyui server 478 | folder_name = "temp" 479 | 480 | # Save image to byte data 481 | byte_data = io.BytesIO() 482 | image.save(byte_data, format="PNG") 483 | byte_data.seek(0) 484 | 485 | # Upload image 486 | try: 487 | resp = self.session.post( 488 | f"http://{self.SERVER_ADDRESS}/upload/image", 489 | files={"image": ("temp.png", byte_data)}, 490 | data={"subfolder": folder_name}, 491 | ) 492 | resp.raise_for_status() 493 | 494 | resp_json = resp.json() 495 | if "name" not in resp_json or "subfolder" not in resp_json: 496 | raise ValueError("Invalid upload response: missing required fields") 497 | 498 | # Set image path 499 | self.comfyui_prompt[key_id]["inputs"]["image"] = ( 500 | resp_json.get("subfolder") + "/" + resp_json.get("name") 501 | ) 502 | except requests.RequestException as e: 503 | raise ConnectionError(f"Failed to upload image: {e}") 504 | except Exception as e: 505 | raise RuntimeError(f"Error processing image upload: {e}") 506 | 507 | if self.debug: 508 | print(f"Set data for {key} (id: {key_id}): {self.comfyui_prompt[key_id]}") 509 | 510 | def find_key_by_title(self, target_title): 511 | target_title = target_title.strip() 512 | for key, value in self.comfyui_prompt.items(): 513 | # Check class_type first 514 | class_type = value.get("class_type", "").strip() 515 | if class_type == target_title: 516 | return key 517 | # Then check title 518 | title = value.get("_meta", {}).get("title", "").strip() 519 | if title == target_title: 520 | return key 521 | if self.debug: 522 | print(f"Key not found: {target_title}") 523 | return None 524 | 525 | def generate(self, node_names=None) -> dict: 526 | node_ids = {} 527 | if node_names is not None: 528 | for node_name in node_names: 529 | node_id = self.find_key_by_title(node_name) 530 | if node_id is not None: 531 | node_ids[node_id] = node_name 532 | 533 | images, text = self.get_images(self.comfyui_prompt) 534 | results = {} 535 | for node_id, node_images in images.items(): 536 | if node_id in node_ids: 537 | for image_data in node_images: 538 | image = Image.open(io.BytesIO(image_data)) 539 | results[node_ids[node_id]] = image 540 | for node_id, node_text in text.items(): 541 | if node_id in node_ids: 542 | results[node_ids[node_id]] = node_text 543 | 544 | return results 545 | 546 | 547 | def main(): 548 | comfyui_client = None 549 | try: 550 | comfyui_client = ComfyUIClient( 551 | "192.168.1.27:8188", "workflow_api.json", debug=True 552 | ) 553 | comfyui_client.connect() 554 | comfyui_client.set_data(key="KSampler", seed=random.randint(0, sys.maxsize)) 555 | comfyui_client.set_data( 556 | key="CLIP Text Encode Positive", text="beautiful landscape painting" 557 | ) 558 | for key, image in comfyui_client.generate(["Result Image"]).items(): 559 | image.save(f"{key}.png") 560 | if comfyui_client.debug: 561 | print(f"Saved {key}.png") 562 | except Exception as e: 563 | print(f"Error in main: {e}") 564 | finally: 565 | if comfyui_client is not None: 566 | comfyui_client.close() 567 | 568 | 569 | async def main_async(): 570 | comfyui_client = None 571 | try: 572 | comfyui_client = ComfyUIClientAsync( 573 | "192.168.1.27:8188", "workflow_api.json", debug=True 574 | ) 575 | await comfyui_client.connect() 576 | await comfyui_client.set_data( 577 | key="KSampler", seed=random.randint(0, sys.maxsize) 578 | ) 579 | await comfyui_client.set_data( 580 | key="CLIP Text Encode Positive", text="beautiful landscape painting" 581 | ) 582 | for key, image in (await comfyui_client.generate(["Result Image"])).items(): 583 | image.save(f"{key}_async.png") 584 | if comfyui_client.debug: 585 | print(f"Saved {key}_async.png") 586 | except Exception as e: 587 | print(f"Error in main_async: {e}") 588 | finally: 589 | if comfyui_client is not None: 590 | await comfyui_client.close() 591 | 592 | 593 | if __name__ == "__main__": 594 | # non-async 595 | main() 596 | 597 | # async 598 | import asyncio 599 | 600 | asyncio.run(main_async()) 601 | --------------------------------------------------------------------------------