├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── comfyui_custom_nodes ├── webui_io.py ├── webui_proxy_nodes.py └── webui_save_image.py ├── comfyui_custom_scripts └── extensions │ ├── webuiEvents.js │ ├── webuiNodes.js │ ├── webuiPatches.js │ ├── webuiRequests.js │ └── webuiTypes.js ├── install.py ├── install_comfyui.py ├── javascript └── compute-height.js ├── lib_comfyui ├── argv_conversion.py ├── comfyui │ ├── iframe_requests.py │ ├── pre_main.py │ ├── queue_tracker.py │ ├── routes_extension.py │ └── type_conversion.py ├── comfyui_process.py ├── custom_extension_injector.py ├── default_workflow_types.py ├── external_code │ ├── __init__.py │ └── api.py ├── find_extensions.py ├── global_state.py ├── ipc │ ├── __init__.py │ ├── callback.py │ ├── payload.py │ └── strategies.py ├── platform_utils.py ├── torch_utils.py └── webui │ ├── accordion.py │ ├── callbacks.py │ ├── gradio_utils.py │ ├── patches.py │ ├── paths.py │ ├── proxies.py │ ├── reverse_proxy.py │ ├── settings.py │ └── tab.py ├── preload.py ├── requirements-test.txt ├── requirements.txt ├── resources └── front-page.gif ├── scripts └── comfyui.py ├── style.css └── tests ├── __init__.py ├── lib_comfyui_tests ├── __init__.py ├── argv_conversion_test.py ├── external_code_tests │ ├── __init__.py │ ├── import_test.py │ └── run_workflow_test.py └── ipc_tests │ ├── __init__.py │ └── payload_test.py └── utils.py /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run basic features tests on CPU with empty SD model 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout sd-webui-comfyui 12 | uses: actions/checkout@v3 13 | 14 | - name: Checkout stable-diffusion-webui 15 | uses: actions/checkout@v3 16 | with: 17 | repository: 'AUTOMATIC1111/stable-diffusion-webui' 18 | path: 'stable-diffusion-webui' 19 | 20 | - name: Set up Python 3.10 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.10.6 24 | cache: pip 25 | cache-dependency-path: | 26 | **/requirements*txt 27 | stable-diffusion-webui/launch.py 28 | 29 | - name: Install test dependencies 30 | run: | 31 | pip install -r requirements.txt -r requirements-test.txt 32 | env: 33 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 34 | PIP_PROGRESS_BAR: "off" 35 | 36 | - name: Setup environment 37 | run: | 38 | cd stable-diffusion-webui 39 | python launch.py --skip-torch-cuda-test --exit 40 | env: 41 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 42 | PIP_PROGRESS_BAR: "off" 43 | TORCH_INDEX_URL: https://download.pytorch.org/whl/cpu 44 | WEBUI_LAUNCH_LIVE_OUTPUT: "1" 45 | PYTHONUNBUFFERED: "1" 46 | 47 | - name: Run tests 48 | run: | 49 | python -m pytest -vv --junitxml=tests/results.xml tests 50 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | 133 | /ComfyUI/ 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ModelSurge 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sd-webui-comfyui 2 | ## Overview 3 | sd-webui-comfyui is an extension for [A1111 webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) that embeds [ComfyUI](https://github.com/comfyanonymous/ComfyUI) workflows in different sections of the normal pipeline of the webui. This allows to create ComfyUI nodes that interact directly with some parts of the webui's normal pipeline. 4 | 5 |  6 | 7 | ## Features 8 | - Use ComfyUI directly into the Webui 9 | - Support for [loading custom nodes from other Webui extensions](https://github.com/ModelSurge/sd-webui-comfyui/wiki/Developing-custom-nodes-from-webui-extensions) 10 | - Integration of [ComfyUI workflows](https://github.com/ModelSurge/sd-webui-comfyui/wiki/Developing-custom-workflow-types) directly into the Webui's pipeline, such as `preprocess`, `preprocess (latent)`, `unet`, `postprocess (latent)`, `postprocess`, `transformer text encode`, etc. 11 | - Webui nodes for sharing resources and data, such as the model, the prompt, etc. 12 | 13 | For a full overview of all the advantageous features this extension adds to ComfyUI and to the Webui, check out the [wiki page](https://github.com/ModelSurge/sd-webui-comfyui/wiki). 14 | 15 | ## Officially supported versions 16 | - A1111 Webui >= `1.5.1` 17 | - ComfyUI == `latest` 18 | 19 | ## Installation 20 | 1) Go to Extensions > Available 21 | 2) Click the `Load from:` button 22 | 3) Enter "ComfyUI" in the search bar 23 | 4) Click the `Install` button of the ComfyUI Tab cell 24 | 5) Restart the webui 25 | 6) Go to the `ComfyUI` tab, and follow the instructions 26 | 27 | ## Remote users, reverse proxies, etc. 28 | The extension is now able to load comfyui for remote users using a local reverse proxy. 29 | This is necessary when the webui is started remotely, for example when: 30 | - using the command line arguments `--share`, or `--ngrok` 31 | - using reverse proxy options of the [sd-webui-tunnels](https://github.com/Bing-su/sd-webui-tunnels) extension 32 | 33 | If you want the extension to keep the reverse proxy disabled or always enable it for some reason, you can update your preferences in the settings tab. 34 | 35 | To start the reverse proxy, the extension needs the command line argument `--api` for the webui, which starts a fastapi server. 36 | Without fastapi, the extension will not be able to create a reverse proxy for comfyui, and then remote browsers will not be able to load comfyui iframes. 37 | 38 | In practice, if the webui url is `http://localhost:7860`, then the extension effectively creates two reverse proxies: 39 | - An HTTP reverse proxy at POST, GET, PUT and DELETE http://localhost:7860/sd-webui-comfyui/comfyui 40 | - A websockets reverse proxy at ws://localhost:7860/sd-webui-comfyui/comfyui/ws 41 | 42 | ## Contributing 43 | We welcome contributions from anyone who is interested in improving sd-webui-comfyui. If you would like to contribute, please follow these steps: 44 | 45 | 1) Fork the repository and create a new branch for your feature or bug fix. 46 | 2) Implement your changes, adding any necessary documentation and tests. 47 | 3) Submit a pull request. 48 | 4) We will review your contribution as soon as possible and provide feedback. 49 | 50 | ## License 51 | MIT 52 | 53 | ## Contact 54 | If you have any questions or concerns, please leave an issue, or start a thread in the discussions. 55 | 56 | Thank you for your interest! 57 | -------------------------------------------------------------------------------- /comfyui_custom_nodes/webui_io.py: -------------------------------------------------------------------------------- 1 | from lib_comfyui import global_state 2 | 3 | 4 | class StaticProperty(object): 5 | def __init__(self, f): 6 | self.f = f 7 | 8 | def __get__(self, *args): 9 | return self.f() 10 | 11 | 12 | class FromWebui: 13 | @classmethod 14 | def INPUT_TYPES(cls): 15 | return { 16 | "required": { 17 | "void": ("VOID", ), 18 | }, 19 | } 20 | 21 | @StaticProperty 22 | def RETURN_TYPES(): 23 | return getattr(global_state, "current_workflow_input_types", ()) 24 | 25 | RETURN_NAMES = () 26 | FUNCTION = "get_node_inputs" 27 | 28 | CATEGORY = "webui" 29 | 30 | @staticmethod 31 | def get_node_inputs(void): 32 | return global_state.node_inputs 33 | 34 | 35 | class ToWebui: 36 | @classmethod 37 | def INPUT_TYPES(cls): 38 | return { 39 | "required": {}, 40 | } 41 | RETURN_TYPES = () 42 | FUNCTION = "extend_node_outputs" 43 | 44 | CATEGORY = "webui" 45 | 46 | OUTPUT_NODE = True 47 | 48 | @staticmethod 49 | def extend_node_outputs(**outputs): 50 | global_state.node_outputs += [outputs] 51 | return () 52 | 53 | 54 | NODE_CLASS_MAPPINGS = { 55 | "FromWebui": FromWebui, 56 | "ToWebui": ToWebui, 57 | } 58 | 59 | NODE_DISPLAY_NAME_MAPPINGS = { 60 | "FromWebui": "From Webui", 61 | "ToWebui": "To Webui", 62 | } 63 | -------------------------------------------------------------------------------- /comfyui_custom_nodes/webui_proxy_nodes.py: -------------------------------------------------------------------------------- 1 | from lib_comfyui.webui import proxies 2 | from lib_comfyui import global_state 3 | 4 | 5 | class WebuiCheckpointLoader: 6 | @classmethod 7 | def INPUT_TYPES(s): 8 | return { 9 | "required": { 10 | "void": ("VOID", ), 11 | }, 12 | } 13 | RETURN_TYPES = ("MODEL", "CLIP", "VAE") 14 | FUNCTION = "load_checkpoint" 15 | 16 | CATEGORY = "loaders" 17 | 18 | def load_checkpoint(self, void): 19 | config = proxies.get_comfy_model_config() 20 | proxies.raise_on_unsupported_model_type(config) 21 | return ( 22 | proxies.ModelPatcher(proxies.Model()), 23 | proxies.ClipWrapper(proxies.Clip()), 24 | proxies.VaeWrapper(proxies.Vae()), 25 | ) 26 | 27 | 28 | class WebuiPrompts: 29 | @classmethod 30 | def INPUT_TYPES(s): 31 | return { 32 | "required": { 33 | "void": ("VOID", ), 34 | }, 35 | } 36 | RETURN_TYPES = ("STRING", "STRING") 37 | RETURN_NAMES = ("positive", "negative") 38 | FUNCTION = "get_prompts" 39 | 40 | CATEGORY = "text" 41 | 42 | def get_prompts(self, void): 43 | positive_prompts, _extra_networks = proxies.extra_networks_parse_prompts([getattr(global_state, 'last_positive_prompt', '')]) 44 | 45 | return ( 46 | positive_prompts[0], 47 | getattr(global_state, 'last_negative_prompt', ''), 48 | ) 49 | 50 | 51 | NODE_CLASS_MAPPINGS = { 52 | "WebuiCheckpointLoader": WebuiCheckpointLoader, 53 | "WebuiPrompts": WebuiPrompts, 54 | } 55 | 56 | NODE_DISPLAY_NAME_MAPPINGS = { 57 | "WebuiCheckpointLoader": 'Webui Checkpoint', 58 | "WebuiPrompts": "Webui Prompts", 59 | } 60 | -------------------------------------------------------------------------------- /comfyui_custom_nodes/webui_save_image.py: -------------------------------------------------------------------------------- 1 | from lib_comfyui.webui.settings import opts 2 | from torchvision.transforms.functional import to_pil_image 3 | from lib_comfyui.webui.paths import webui_save_image 4 | 5 | 6 | class WebuiSaveImage: 7 | @classmethod 8 | def INPUT_TYPES(cls): 9 | return { 10 | "required": { 11 | "location": (["txt2img-images", "img2img-images", "extras-images", "txt2img-grids", "img2img-grids", ], ), 12 | "images": ("IMAGE", ), 13 | }, 14 | } 15 | RETURN_TYPES = () 16 | FUNCTION = "save_image" 17 | 18 | CATEGORY = "image" 19 | 20 | OUTPUT_NODE = True 21 | 22 | def save_image(self, location, images): 23 | if 'txt2img' in location: 24 | output_dir = opts.outdir_samples or opts.outdir_txt2img_samples if 'images' in location else opts.outdir_grids or opts.outdir_txt2img_grids 25 | elif 'img2img' in location: 26 | output_dir = opts.outdir_samples or opts.outdir_img2img_samples if 'images' in location else opts.outdir_grids or opts.outdir_img2img_grids 27 | else: 28 | output_dir = opts.outdir_samples or opts.outdir_extras_samples 29 | 30 | for image in images: 31 | pil_image = to_pil_image(image.permute(2, 0, 1)) 32 | filename, _ = webui_save_image(image=pil_image, relative_path=output_dir, basename='') 33 | 34 | return [] 35 | 36 | 37 | NODE_CLASS_MAPPINGS = { 38 | "WebuiSaveImage": WebuiSaveImage, 39 | } 40 | 41 | NODE_DISPLAY_NAME_MAPPINGS = { 42 | "WebuiSaveImage": 'Webui Save Image', 43 | } 44 | -------------------------------------------------------------------------------- /comfyui_custom_scripts/extensions/webuiEvents.js: -------------------------------------------------------------------------------- 1 | import { app } from "/scripts/app.js"; 2 | import { api } from "/scripts/api.js"; 3 | 4 | 5 | const POLLING_TIMEOUT = 500; 6 | 7 | 8 | const iframeRegisteredEvent = new Promise(async (resolve, reject) => { 9 | const searchParams = new URLSearchParams(window.location.search); 10 | const workflowTypeId = searchParams.get("workflowTypeId"); 11 | const webuiClientId = searchParams.get("webuiClientId"); 12 | 13 | if (!workflowTypeId || !webuiClientId) { 14 | reject("Cannot identify comfyui client: search params missing."); 15 | } 16 | else { 17 | const workflowTypeInfo = await fetchWorkflowTypeInfo(workflowTypeId); 18 | resolve({ 19 | workflowTypeId, 20 | webuiClientId, 21 | ...workflowTypeInfo, 22 | }); 23 | } 24 | }); 25 | 26 | async function fetchWorkflowTypeInfo(workflowTypeId) { 27 | const response = await api.fetchApi("/sd-webui-comfyui/workflow_type?" + new URLSearchParams({ 28 | workflowTypeId, 29 | }), { 30 | method: "GET", 31 | headers: {"Content-Type": "application/json"}, 32 | cache: "no-store", 33 | }); 34 | return await response.json(); 35 | } 36 | 37 | const appReadyEvent = new Promise(resolve => { 38 | const appReadyOrRecursiveSetTimeout = () => { 39 | if (app.graph && window.name) { 40 | resolve(); 41 | } 42 | else { 43 | setTimeout(appReadyOrRecursiveSetTimeout, POLLING_TIMEOUT); 44 | } 45 | }; 46 | appReadyOrRecursiveSetTimeout(); 47 | }); 48 | 49 | 50 | export { 51 | iframeRegisteredEvent, 52 | appReadyEvent, 53 | } 54 | -------------------------------------------------------------------------------- /comfyui_custom_scripts/extensions/webuiNodes.js: -------------------------------------------------------------------------------- 1 | import { app } from "/scripts/app.js"; 2 | import { iframeRegisteredEvent } from "/extensions/webui_scripts/sd-webui-comfyui/extensions/webuiEvents.js"; 3 | import { isString, getTypesLength } from "/extensions/webui_scripts/sd-webui-comfyui/extensions/webuiTypes.js"; 4 | 5 | 6 | function createVoidWidget(node, name) { 7 | const widget = { 8 | type: "customtext", 9 | name, 10 | get value() { 11 | return `${Math.random()}${Date.now()}`; 12 | }, 13 | set value(x) {}, 14 | }; 15 | widget.parent = node; 16 | node.addCustomWidget(widget); 17 | 18 | return widget; 19 | } 20 | 21 | app.registerExtension({ 22 | name: "sd-webui-comfyui", 23 | async getCustomWidgets(app) { 24 | return { 25 | VOID(node, inputName) { 26 | createVoidWidget(node, inputName); 27 | }, 28 | }; 29 | }, 30 | async addCustomNodeDefs(defs) { 31 | let iframeInfo = null; 32 | 33 | try { 34 | iframeInfo = await iframeRegisteredEvent; 35 | } 36 | catch { 37 | return; 38 | } 39 | 40 | const nodes = webuiIoNodeNames.map(name => defs[name]); 41 | for (const node of nodes) { 42 | node.display_name = `${node.display_name} - ${iframeInfo.displayName}`; 43 | 44 | if (node.name === 'FromWebui') { 45 | let outputs = iframeInfo.webuiIoTypes.outputs; 46 | if (isString(outputs)) { 47 | outputs = [outputs]; 48 | } 49 | const are_types_array = Array.isArray(outputs); 50 | for (const k in outputs) { 51 | const v = outputs[k]; 52 | node.output_name.push(are_types_array ? v : k); 53 | node.output_is_list.push(false); 54 | node.output.push(v); 55 | } 56 | } 57 | else if (node.name === 'ToWebui') { 58 | let inputs = iframeInfo.webuiIoTypes.inputs; 59 | if (isString(inputs)) { 60 | node.input.required[inputs] = [inputs]; 61 | } 62 | else { 63 | for (const k in inputs) { 64 | const v = inputs[k]; 65 | node.input.required[k] = [v]; 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | async nodeCreated(node) { 72 | let iframeInfo = null; 73 | 74 | try { 75 | iframeInfo = await iframeRegisteredEvent; 76 | } catch { 77 | return; 78 | } 79 | 80 | if (!webuiIoNodeNames.includes(node.type)) { 81 | return; 82 | } 83 | 84 | const maxIoLength = Math.max( 85 | getTypesLength(iframeInfo.webuiIoTypes.outputs), 86 | getTypesLength(iframeInfo.webuiIoTypes.inputs), 87 | ); 88 | // 260 and 40 are empirical values that seem to work 89 | node.size = [260, 40 + distanceBetweenIoSlots * maxIoLength]; 90 | }, 91 | async setup() { 92 | app.loadGraphData(); 93 | }, 94 | }); 95 | 96 | const webuiIoNodeNames = [ 97 | 'FromWebui', 98 | 'ToWebui', 99 | ]; 100 | 101 | const distanceBetweenIoSlots = 20; 102 | -------------------------------------------------------------------------------- /comfyui_custom_scripts/extensions/webuiPatches.js: -------------------------------------------------------------------------------- 1 | import { app } from "/scripts/app.js"; 2 | import { api } from "/scripts/api.js"; 3 | import { iframeRegisteredEvent } from "/extensions/webui_scripts/sd-webui-comfyui/extensions/webuiEvents.js"; 4 | import { getTypesLength } from "/extensions/webui_scripts/sd-webui-comfyui/extensions/webuiTypes.js"; 5 | 6 | 7 | async function patchUiEnv(iframeInfo) { 8 | if (iframeInfo.workflowTypeId.endsWith('_txt2img') || iframeInfo.workflowTypeId.endsWith('_img2img')) { 9 | const menuToHide = document.querySelector('.comfy-menu'); 10 | menuToHide.style.display = 'none'; 11 | patchSavingMechanism(); 12 | } 13 | 14 | await patchDefaultGraph(iframeInfo); 15 | } 16 | 17 | function patchSavingMechanism() { 18 | app.graph.original_serialize = app.graph.serialize; 19 | app.graph.patched_serialize = () => JSON.parse(localStorage.getItem('workflow')); 20 | app.graph.serialize = app.graph.patched_serialize; 21 | 22 | app.original_graphToPrompt = app.graphToPrompt; 23 | app.patched_graphToPrompt = () => { 24 | app.graph.serialize = app.graph.original_serialize; 25 | const result = app.original_graphToPrompt(); 26 | app.graph.serialize = app.graph.patched_serialize; 27 | return result; 28 | }; 29 | app.graphToPrompt = app.patched_graphToPrompt; 30 | 31 | const saveButton = document.querySelector('#comfy-save-button'); 32 | saveButton.removeAttribute('id'); 33 | const comfyParent = saveButton.parentElement; 34 | const muahahaButton = document.createElement('button'); 35 | muahahaButton.setAttribute('id', 'comfy-save-button'); 36 | comfyParent.appendChild(muahahaButton); 37 | muahahaButton.click = () => { 38 | app.graph.serialize = app.graph.original_serialize; 39 | saveButton.click(); 40 | app.graph.serialize = app.graph.patched_serialize; 41 | }; 42 | } 43 | 44 | async function patchDefaultGraph(iframeInfo) { 45 | // preserve the normal default graph 46 | if (!iframeInfo.defaultWorkflow) { 47 | return; 48 | } 49 | 50 | app.original_loadGraphData = app.loadGraphData; 51 | const doLoadGraphData = graphData => { 52 | if (graphData !== "auto") { 53 | return app.original_loadGraphData(graphData); 54 | } 55 | 56 | app.graph.clear(); 57 | 58 | const from_webui = LiteGraph.createNode("FromWebui"); 59 | const to_webui = LiteGraph.createNode("ToWebui"); 60 | 61 | if (!from_webui || !to_webui) { 62 | return; 63 | } 64 | 65 | app.graph.add(from_webui); 66 | app.graph.add(to_webui); 67 | 68 | const typesLength = getTypesLength(iframeInfo.webuiIoTypes.outputs); 69 | for (let i = 0; i < typesLength; ++i) { 70 | from_webui.connect(i, to_webui, i); 71 | } 72 | 73 | app.graph.arrange(); 74 | }; 75 | 76 | app.loadGraphData = (graphData) => { 77 | if (graphData) { 78 | return doLoadGraphData(graphData); 79 | } 80 | else { 81 | return doLoadGraphData(iframeInfo.defaultWorkflow); 82 | } 83 | }; 84 | 85 | app.loadGraphData(); 86 | } 87 | 88 | 89 | export { 90 | patchUiEnv, 91 | } 92 | -------------------------------------------------------------------------------- /comfyui_custom_scripts/extensions/webuiRequests.js: -------------------------------------------------------------------------------- 1 | import { app } from "/scripts/app.js"; 2 | import { api } from "/scripts/api.js"; 3 | import { patchUiEnv } from "/extensions/webui_scripts/sd-webui-comfyui/extensions/webuiPatches.js"; 4 | import { iframeRegisteredEvent, appReadyEvent } from "/extensions/webui_scripts/sd-webui-comfyui/extensions/webuiEvents.js"; 5 | 6 | 7 | async function setupWebuiRequestsEnvironment() { 8 | const iframeInfo = await iframeRegisteredEvent; 9 | await appReadyEvent; 10 | await patchUiEnv(iframeInfo); 11 | 12 | function addWebuiRequestListener(type, callback, options) { 13 | api.addEventListener(`webui_${type}`, async (data) => { 14 | api.fetchApi("/sd-webui-comfyui/webui_ws_response", { 15 | method: "POST", 16 | headers: {"Content-Type": "application/json"}, 17 | cache: "no-store", 18 | body: JSON.stringify({response: await callback(data)}), 19 | }); 20 | console.log(`[sd-webui-comfyui] WEBUI REQUEST - ${iframeInfo.workflowTypeId} - ${type}`); 21 | }, options); 22 | }; 23 | 24 | webuiRequests.forEach((request, type) => addWebuiRequestListener(type, request)); 25 | await registerClientToWebui(iframeInfo.workflowTypeId, iframeInfo.webuiClientId, window.name); 26 | console.log(`[sd-webui-comfyui][comfyui] INITIALIZED WS - ${iframeInfo.displayName}`); 27 | } 28 | 29 | async function registerClientToWebui(workflowTypeId, webuiClientId, sid) { 30 | await api.fetchApi("/sd-webui-comfyui/webui_register_client", { 31 | method: "POST", 32 | headers: {"Content-Type": "application/json"}, 33 | cache: "no-store", 34 | body: JSON.stringify({ 35 | workflowTypeId, 36 | webuiClientId, 37 | sid, 38 | }), 39 | }); 40 | } 41 | 42 | const webuiRequests = new Map([ 43 | ["queue_prompt", async (json) => { 44 | await app.queuePrompt(json.detail.queueFront ? -1 : 0, 1); 45 | }], 46 | ["serialize_graph", (json) => { 47 | return app.graph.original_serialize(); 48 | }], 49 | ["set_workflow", (json) => { 50 | app.loadGraphData(json.detail.workflow); 51 | }], 52 | ]); 53 | 54 | setupWebuiRequestsEnvironment(); 55 | -------------------------------------------------------------------------------- /comfyui_custom_scripts/extensions/webuiTypes.js: -------------------------------------------------------------------------------- 1 | function isString(value) { 2 | return typeof value === "string" || value instanceof String; 3 | } 4 | 5 | function getTypesLength(types) { 6 | if (isString(types)) { 7 | return 1; 8 | } 9 | else if (Array.isArray(types)) { 10 | return types.length; 11 | } 12 | else { 13 | return Object.keys(types).length; 14 | } 15 | } 16 | 17 | 18 | export { 19 | isString, 20 | getTypesLength, 21 | } 22 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | import pathlib 3 | import pkg_resources 4 | import re 5 | import sys 6 | import traceback 7 | 8 | 9 | req_file = pathlib.Path(__file__).resolve().parent / "requirements.txt" 10 | req_re = re.compile('^([^=<>~]*)\s*(?:([=<>~])=\s*([^=<>~]*))?$') 11 | 12 | 13 | with open(req_file) as file: 14 | for package in file: 15 | try: 16 | package = package.strip() 17 | match = req_re.search(package) 18 | package_name = match.group(1) 19 | 20 | try: 21 | installed_version = pkg_resources.get_distribution(package_name).version 22 | except Exception: 23 | installed_version = None 24 | pass # package not installed, we still want to install it 25 | 26 | package_already_installed = installed_version is not None 27 | install_info = f"sd-webui-comfyui requirement: {package}" 28 | comparison, required_version = match.group(2, 3) 29 | 30 | if package_already_installed: 31 | install_info = f"sd-webui-comfyui requirement: changing {package_name} version from {installed_version} to {required_version}" 32 | if ( 33 | comparison == '~' or 34 | required_version is None or 35 | eval(f'"{installed_version}" {comparison}= "{required_version}"') 36 | ): 37 | continue 38 | 39 | launch.run_pip(f"install {package}", install_info) 40 | except Exception as e: 41 | print(traceback.format_exception_only(e)) 42 | print(f'Failed to install sd-webui-comfyui requirement: {package}', file=sys.stderr) 43 | -------------------------------------------------------------------------------- /install_comfyui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | default_install_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ComfyUI') 6 | 7 | 8 | def main(install_location, should_install_manager=False): 9 | repo_url = 'https://github.com/comfyanonymous/ComfyUI.git' 10 | install_repo(repo_url, install_location) 11 | 12 | if should_install_manager: 13 | manager_repo_url = 'https://github.com/ltdrdata/ComfyUI-Manager.git' 14 | manager_location = manager_location_from_comfyui_location(install_location) 15 | install_repo(manager_repo_url, manager_location) 16 | 17 | 18 | def manager_location_from_comfyui_location(comfyui_location): 19 | return os.path.join(comfyui_location, 'custom_nodes', 'ComfyUI-Manager') 20 | 21 | 22 | def install_repo(git_repo_url, install_location): 23 | import git 24 | os.mkdir(install_location) 25 | git.Repo.clone_from(git_repo_url, install_location) 26 | 27 | 28 | def update(install_location): 29 | print("[sd-webui-comfyui]", f"Updating comfyui at {install_location}...") 30 | if not install_location.is_dir() or not any(install_location.iterdir()): 31 | print("[sd-webui-comfyui]", f"Cannot update comfyui since it is not installed.", file=sys.stderr) 32 | return 33 | 34 | import git 35 | repo = git.Repo(install_location) 36 | current = repo.head.commit 37 | repo.remotes.origin.pull() 38 | if current == repo.head.commit: 39 | print("[sd-webui-comfyui]", "Already up to date.") 40 | else: 41 | print("[sd-webui-comfyui]", "Done updating comfyui.") 42 | 43 | 44 | if __name__ == '__main__': 45 | install_location = default_install_location 46 | if len(sys.argv) > 1: 47 | inistall_location = sys.argv[1] 48 | 49 | main(install_location) 50 | -------------------------------------------------------------------------------- /javascript/compute-height.js: -------------------------------------------------------------------------------- 1 | const SD_WEBUI_COMFYUI_POLLING_TIMEOUT = 500; 2 | 3 | 4 | function uuidv4() { 5 | return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); 6 | } 7 | 8 | const WEBUI_CLIENT_ID = uuidv4(); 9 | 10 | function changeDisplayedWorkflowType(targetWorkflowType) { 11 | const targetIFrameElement = getWorkflowTypeIFrame(targetWorkflowType); 12 | const currentIFrameElement = targetIFrameElement.parentElement.querySelector(".comfyui-workflow-type-visible"); 13 | currentIFrameElement.classList.remove("comfyui-workflow-type-visible"); 14 | targetIFrameElement.classList.add("comfyui-workflow-type-visible"); 15 | } 16 | 17 | document.addEventListener("DOMContentLoaded", () => { 18 | onComfyuiTabLoaded(clearEnabledDisplayNames); 19 | onComfyuiTabLoaded(setupComfyuiTabEvents); 20 | }); 21 | 22 | function onComfyuiTabLoaded(callback) { 23 | if (getClearEnabledDisplayNamesButtons().some(e => e === null) || 24 | getWorkflowTypeIds() === null || 25 | getComfyuiContainer() === null || 26 | getTabNav() === null || 27 | getWebuiClientIdTextArea() === null 28 | ) { 29 | // webui not yet ready, try again in a bit 30 | setTimeout(() => { onComfyuiTabLoaded(callback); }, SD_WEBUI_COMFYUI_POLLING_TIMEOUT); 31 | return; 32 | } 33 | 34 | callback(); 35 | } 36 | 37 | function clearEnabledDisplayNames() { 38 | for (const clearButton of getClearEnabledDisplayNamesButtons()) { 39 | clearButton.click(); 40 | } 41 | } 42 | 43 | function setupComfyuiTabEvents() { 44 | setupWebuiClientId(); 45 | setupResizeTabEvent(); 46 | setupToggleFooterEvent(); 47 | 48 | updateComfyuiTabHeight(); 49 | 50 | getWorkflowTypeIds().forEach(id => setupIFrame(id)); 51 | } 52 | 53 | function reloadComfyuiIFrames() { 54 | getWorkflowTypeIds().forEach(id => { 55 | setupIFrame(id); 56 | }); 57 | } 58 | 59 | function setupWebuiClientId() { 60 | const textArea = getWebuiClientIdTextArea(); 61 | textArea.value = WEBUI_CLIENT_ID; 62 | textArea.dispatchEvent(new Event('input')); 63 | } 64 | 65 | function setupResizeTabEvent() { 66 | window.addEventListener("resize", updateComfyuiTabHeight); 67 | } 68 | 69 | function setupToggleFooterEvent() { 70 | new MutationObserver((mutationsList) => { 71 | for (const mutation of mutationsList) { 72 | if (mutation.type === 'attributes' && mutation.attributeName === 'style') { 73 | updateFooterStyle(); 74 | } 75 | } 76 | }) 77 | .observe(getComfyuiTab(), { attributes: true }); 78 | } 79 | 80 | function updateComfyuiTabHeight() { 81 | const container = getComfyuiContainer(); 82 | const tabNavBottom = getTabNav().getBoundingClientRect().bottom; 83 | container.style.height = `calc(100% - ${tabNavBottom}px)`; 84 | } 85 | 86 | function updateFooterStyle() { 87 | const tabDisplay = getComfyuiTab().style.display; 88 | const footer = getFooter(); 89 | 90 | if(footer === null) return; 91 | if(tabDisplay === 'block') { 92 | footer.classList.add('comfyui-remove-display'); 93 | } 94 | else { 95 | footer.classList.remove('comfyui-remove-display'); 96 | } 97 | } 98 | 99 | function getClearEnabledDisplayNamesButtons() { 100 | return [ 101 | document.getElementById("script_txt2img_comfyui_clear_enabled_display_names") ?? document.getElementById("script_txt2txt_comfyui_clear_enabled_display_names") ?? null, 102 | document.getElementById("script_img2img_comfyui_clear_enabled_display_names") ?? null, 103 | ]; 104 | } 105 | 106 | function getTabNav() { 107 | const tabs = document.getElementById("tabs") ?? null; 108 | return tabs ? tabs.querySelector(".tab-nav") : null; 109 | } 110 | 111 | function getComfyuiTab() { 112 | return document.getElementById("tab_comfyui_webui_root") ?? null; 113 | } 114 | 115 | function getComfyuiContainer() { 116 | return document.getElementById("comfyui_webui_container") ?? null; 117 | } 118 | 119 | function getWebuiClientIdTextArea() { 120 | return document.querySelector("#comfyui_webui_client_id textarea") ?? null; 121 | } 122 | 123 | function getFooter() { 124 | return document.querySelector('#footer') ?? null; 125 | } 126 | 127 | function getWorkflowTypeIFrame(workflowTypeId) { 128 | return document.querySelector(`[workflow_type_id="${workflowTypeId}"]`); 129 | } 130 | 131 | function getWorkflowTypeIds() { 132 | return getExtensionDynamicProperty('workflow_type_ids'); 133 | } 134 | 135 | function getExtensionDynamicProperty(key) { 136 | return JSON.parse(document.querySelector(`[sd_webui_comfyui_key="${key}"]`)?.innerText ?? "null"); 137 | } 138 | 139 | function reloadFrameElement(iframeElement) { 140 | iframeElement.src += ""; 141 | } 142 | 143 | function setupIFrame(workflowTypeId) { 144 | let messageToReceive = workflowTypeId; 145 | 146 | const iframeSearchParams = new URLSearchParams(); 147 | iframeSearchParams.set("workflowTypeId", workflowTypeId); 148 | iframeSearchParams.set("webuiClientId", WEBUI_CLIENT_ID); 149 | const iframe = getWorkflowTypeIFrame(workflowTypeId); 150 | const base_src = iframe.getAttribute("base_src"); 151 | const iframe_src = base_src + "?" + iframeSearchParams.toString(); 152 | if (iframe.src !== iframe_src) { 153 | iframe.src = iframe_src; 154 | } 155 | else { 156 | reloadFrameElement(iframe); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib_comfyui/argv_conversion.py: -------------------------------------------------------------------------------- 1 | from lib_comfyui import ipc 2 | from lib_comfyui.webui import settings 3 | 4 | 5 | COMFYUI_ARGV_PREFIX = 'comfyui_' 6 | 7 | 8 | def get_comfyui_args(): 9 | args = settings.get_additional_argv() + extract_comfyui_argv() 10 | deduplicate_comfyui_args(args) 11 | return args 12 | 13 | 14 | @ipc.restrict_to_process('webui') 15 | def extract_comfyui_argv(): 16 | from modules import shared 17 | result = [] 18 | for k, v in _items(shared.cmd_opts): 19 | if k.startswith(COMFYUI_ARGV_PREFIX): 20 | k = k.replace(COMFYUI_ARGV_PREFIX, '') 21 | result.extend(as_argv_list(k, v)) 22 | return result 23 | 24 | 25 | def as_argv_list(k, v): 26 | result = [] 27 | if is_used_argv(k, v): 28 | result.append(f'--{k.replace("_", "-")}') 29 | if is_paired_argv(k, v): 30 | result.append(str(v)) 31 | return result 32 | 33 | 34 | def deduplicate_comfyui_args(argv): 35 | seen_args = set() 36 | i = 0 37 | while i < len(argv): 38 | arg = argv[i] 39 | if arg in seen_args: 40 | if arg in ('--port',): 41 | len_to_remove = 2 42 | elif arg in ('--listen',): 43 | has_value = i + 1 < len(argv) and not argv[i + 1].startswith('--') 44 | len_to_remove = 1 + int(has_value) 45 | else: 46 | len_to_remove = 1 47 | argv[i:i + len_to_remove] = () 48 | else: 49 | if arg.startswith('--'): 50 | seen_args.add(arg) 51 | i += 1 52 | 53 | 54 | def _items(cmd_opts): 55 | return vars(cmd_opts).items() 56 | 57 | 58 | def is_used_argv(k, v): 59 | return v not in [False, None] 60 | 61 | 62 | def is_paired_argv(k, v): 63 | return v is not True 64 | -------------------------------------------------------------------------------- /lib_comfyui/comfyui/iframe_requests.py: -------------------------------------------------------------------------------- 1 | import json 2 | import multiprocessing 3 | from queue import Empty 4 | from typing import List, Any, Dict, Tuple, Optional 5 | from lib_comfyui import ipc, global_state, torch_utils, external_code 6 | from lib_comfyui.comfyui import queue_tracker 7 | 8 | 9 | class ComfyuiIFrameRequests: 10 | finished_comfyui_queue = multiprocessing.Queue() 11 | server_instance = None 12 | sid_map = {} 13 | 14 | @staticmethod 15 | @ipc.run_in_process('comfyui') 16 | def send(request, workflow_type, data=None): 17 | if data is None: 18 | data = {} 19 | 20 | cls = ComfyuiIFrameRequests 21 | if global_state.focused_webui_client_id is None: 22 | raise RuntimeError('No active webui connection') 23 | 24 | ws_client_ids = cls.sid_map[global_state.focused_webui_client_id] 25 | if workflow_type not in ws_client_ids: 26 | raise RuntimeError(f"The workflow type {workflow_type} has not been registered by the active webui client {global_state.focused_webui_client_id}") 27 | 28 | clear_queue(cls.finished_comfyui_queue) 29 | cls.server_instance.send_sync(request, data, ws_client_ids[workflow_type]) 30 | 31 | return cls.finished_comfyui_queue.get() 32 | 33 | @staticmethod 34 | @ipc.restrict_to_process('webui') 35 | def start_workflow_sync( 36 | batch_input_args: Tuple[Any, ...], 37 | workflow_type_id: str, 38 | workflow_input_types: List[str], 39 | queue_front: bool, 40 | ) -> List[Dict[str, Any]]: 41 | from modules import shared 42 | if shared.state.interrupted: 43 | raise RuntimeError('The workflow was not started because the webui has been interrupted') 44 | 45 | global_state.node_inputs = batch_input_args 46 | global_state.node_outputs = [] 47 | global_state.current_workflow_input_types = workflow_input_types 48 | 49 | try: 50 | queue_tracker.setup_tracker_id() 51 | 52 | # unsafe queue tracking 53 | ComfyuiIFrameRequests.send( 54 | request='webui_queue_prompt', 55 | workflow_type=workflow_type_id, 56 | data={ 57 | 'requiredNodeTypes': [], 58 | 'queueFront': queue_front, 59 | } 60 | ) 61 | 62 | if not queue_tracker.wait_until_done(): 63 | raise RuntimeError('The workflow has not returned normally') 64 | 65 | return global_state.node_outputs 66 | finally: 67 | global_state.current_workflow_input_types = () 68 | global_state.node_outputs = [] 69 | global_state.node_inputs = None 70 | 71 | @staticmethod 72 | @ipc.restrict_to_process('webui') 73 | def validate_amount_of_nodes_or_throw( 74 | workflow_type_id: str, 75 | max_amount_of_FromWebui_nodes: Optional[int], 76 | max_amount_of_ToWebui_nodes: Optional[int], 77 | ) -> None: 78 | workflow_graph = get_workflow_graph(workflow_type_id) 79 | all_nodes = workflow_graph['nodes'] 80 | enabled_nodes = [node for node in all_nodes if node['mode'] != 2] 81 | node_types = [node['type'] for node in enabled_nodes] 82 | amount_of_FromWebui_nodes = len([t for t in node_types if t == 'FromWebui']) 83 | amount_of_ToWebui_nodes = len([t for t in node_types if t == 'ToWebui']) 84 | max_FromWebui_nodes = max_amount_of_FromWebui_nodes if max_amount_of_FromWebui_nodes is not None else amount_of_FromWebui_nodes 85 | max_ToWebui_nodes = max_amount_of_ToWebui_nodes if max_amount_of_ToWebui_nodes is not None else amount_of_ToWebui_nodes 86 | 87 | if amount_of_FromWebui_nodes > max_FromWebui_nodes: 88 | raise TooManyFromWebuiNodesError(f'Unable to run the workflow {workflow_type_id}. ' 89 | f'Expected at most {max_FromWebui_nodes} FromWebui node(s), ' 90 | f'{amount_of_FromWebui_nodes} were found.') 91 | 92 | if amount_of_ToWebui_nodes > max_ToWebui_nodes: 93 | raise TooManyToWebuiNodesError(f'Unable to run the workflow {workflow_type_id}. ' 94 | f'Expected at most {max_ToWebui_nodes} ToWebui node(s), ' 95 | f'{amount_of_ToWebui_nodes} were found.') 96 | 97 | @staticmethod 98 | @ipc.restrict_to_process('comfyui') 99 | def register_client(request) -> None: 100 | workflow_type_id = request['workflowTypeId'] 101 | webui_client_id = request['webuiClientId'] 102 | sid = request['sid'] 103 | 104 | if webui_client_id not in ComfyuiIFrameRequests.sid_map: 105 | ComfyuiIFrameRequests.sid_map[webui_client_id] = {} 106 | 107 | ComfyuiIFrameRequests.sid_map[webui_client_id][workflow_type_id] = sid 108 | print(f'registered ws - {workflow_type_id} - {sid}') 109 | 110 | @staticmethod 111 | @ipc.restrict_to_process('comfyui') 112 | def handle_response(response): 113 | ComfyuiIFrameRequests.finished_comfyui_queue.put(response) 114 | 115 | 116 | def extend_infotext_with_comfyui_workflows(p, tab): 117 | workflows = {} 118 | for workflow_type in external_code.get_workflow_types(tab): 119 | workflow_type_id = workflow_type.get_ids(tab)[0] 120 | try: 121 | ComfyuiIFrameRequests.validate_amount_of_nodes_or_throw( 122 | workflow_type_id, 123 | workflow_type.max_amount_of_FromWebui_nodes, 124 | workflow_type.max_amount_of_ToWebui_nodes, 125 | ) 126 | except RuntimeError: 127 | continue 128 | if not external_code.is_workflow_type_enabled(workflow_type_id): 129 | continue 130 | 131 | workflows[workflow_type.base_id] = get_workflow_graph(workflow_type_id) 132 | 133 | p.extra_generation_params['ComfyUI Workflows'] = json.dumps(workflows) 134 | 135 | 136 | def set_workflow_graph(workflow_json, workflow_type_id): 137 | return ComfyuiIFrameRequests.send( 138 | request='webui_set_workflow', 139 | workflow_type=workflow_type_id, 140 | data={'workflow': workflow_json} 141 | ) 142 | 143 | 144 | def get_workflow_graph(workflow_type_id): 145 | return ComfyuiIFrameRequests.send(request='webui_serialize_graph', workflow_type=workflow_type_id) 146 | 147 | 148 | def clear_queue(queue: multiprocessing.Queue): 149 | while not queue.empty(): 150 | try: 151 | queue.get(timeout=1) 152 | except Empty: 153 | pass 154 | 155 | 156 | class TooManyFromWebuiNodesError(RuntimeError): 157 | pass 158 | 159 | 160 | class TooManyToWebuiNodesError(RuntimeError): 161 | pass 162 | -------------------------------------------------------------------------------- /lib_comfyui/comfyui/pre_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def patch_sys_path(): 6 | comfyui_install_dir = os.getcwd() 7 | extension_dir = os.getenv("SD_WEBUI_COMFYUI_EXTENSION_DIR") 8 | if not comfyui_install_dir or not extension_dir: 9 | print("[sd-webui-comfyui]", f"Could not add new entries to sys.path. install_dir={comfyui_install_dir}, extension_dir={extension_dir}, sys.path={sys.path}", file=sys.stderr) 10 | print("[sd-webui-comfyui]", f"Exiting...", file=sys.stderr) 11 | exit(1) 12 | 13 | sys.path[:0] = (comfyui_install_dir, extension_dir) 14 | 15 | 16 | if __name__ == "__main__": 17 | patch_sys_path() 18 | 19 | 20 | import atexit 21 | import builtins 22 | import psutil 23 | import threading 24 | import time 25 | import runpy 26 | from lib_comfyui import ( 27 | custom_extension_injector, 28 | ipc, 29 | ) 30 | from lib_comfyui.comfyui import routes_extension, queue_tracker 31 | from lib_comfyui.webui import paths, settings 32 | 33 | 34 | original_print = builtins.print 35 | def comfyui_print(*args, **kwargs): 36 | return original_print('[ComfyUI]', *args, **kwargs) 37 | 38 | 39 | @ipc.restrict_to_process('comfyui') 40 | def main(): 41 | builtins.print = comfyui_print 42 | setup_ipc() 43 | patch_comfyui() 44 | start_comfyui() 45 | 46 | 47 | @ipc.restrict_to_process('comfyui') 48 | def setup_ipc(): 49 | print('[sd-webui-comfyui]', 'Setting up IPC...') 50 | ipc_strategy_class_name = os.getenv('SD_WEBUI_COMFYUI_IPC_STRATEGY_CLASS_NAME') 51 | print('[sd-webui-comfyui]', f'Using inter-process communication strategy: {settings.ipc_display_names[ipc_strategy_class_name]}') 52 | ipc_strategy_factory = getattr(ipc.strategies, os.getenv('SD_WEBUI_COMFYUI_IPC_STRATEGY_CLASS_NAME')) 53 | ipc.current_callback_listeners = {'comfyui': ipc.callback.CallbackWatcher(ipc.call_fully_qualified, 'comfyui', ipc_strategy_factory)} 54 | ipc.current_callback_proxies = {'webui': ipc.callback.CallbackProxy('webui', ipc_strategy_factory)} 55 | ipc.start_callback_listeners() 56 | atexit.register(ipc.stop_callback_listeners) 57 | 58 | parent_id = os.getppid() 59 | monitor_thread = threading.Thread(target=watch_webui_exit, args=(parent_id,)) 60 | monitor_thread.start() 61 | 62 | 63 | @ipc.restrict_to_process('comfyui') 64 | def watch_webui_exit(parent_id): 65 | while True: 66 | if not psutil.pid_exists(parent_id): 67 | print("[sd-webui-comfyui]", "The webui has exited, exiting comfyui.") 68 | exit() 69 | 70 | time.sleep(1) 71 | 72 | 73 | @ipc.restrict_to_process('comfyui') 74 | def patch_comfyui(): 75 | print('[sd-webui-comfyui]', 'Patching ComfyUI...') 76 | try: 77 | # workaround for newer versions of comfyui https://github.com/comfyanonymous/ComfyUI/commit/3039b08eb16777431946ed9ae4a63c5466336bff 78 | # remove the try-except to stop supporting older versions 79 | import comfy.options 80 | comfy.options.enable_args_parsing() 81 | except ImportError: 82 | pass 83 | 84 | paths.share_webui_folder_paths() 85 | custom_extension_injector.register_webui_extensions() 86 | routes_extension.patch_server_routes() 87 | queue_tracker.patch_prompt_queue() 88 | 89 | 90 | @ipc.restrict_to_process('comfyui') 91 | def start_comfyui(): 92 | print('[sd-webui-comfyui]', f'Launching ComfyUI with arguments: {" ".join(sys.argv[1:])}') 93 | runpy.run_path(os.path.join(os.getcwd(), 'main.py'), {'comfyui_print': comfyui_print}, '__main__') 94 | 95 | 96 | if __name__ == '__main__': 97 | ipc.current_process_id = 'comfyui' 98 | main() 99 | -------------------------------------------------------------------------------- /lib_comfyui/comfyui/queue_tracker.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import multiprocessing 3 | from lib_comfyui import ipc 4 | 5 | 6 | class PromptQueueTracker: 7 | done_event = multiprocessing.Event() 8 | put_event = multiprocessing.Event() 9 | tracked_id = None 10 | original_id = None 11 | queue_instance = None 12 | server_instance = None 13 | 14 | @staticmethod 15 | def patched__init__(self, server_instance): 16 | prompt_queue = self 17 | PromptQueueTracker.server_instance = server_instance 18 | PromptQueueTracker.queue_instance = self 19 | 20 | def patched_put(item, *args, original_put, **kwargs): 21 | with prompt_queue.mutex: 22 | if abs(item[0]) == PromptQueueTracker.tracked_id: 23 | PromptQueueTracker.put_event.set() 24 | 25 | return original_put(item, *args, **kwargs) 26 | 27 | prompt_queue.put = functools.partial(patched_put, original_put=prompt_queue.put) 28 | 29 | # task_done 30 | def patched_task_done(item_id, output, *args, original_task_done, **kwargs): 31 | with prompt_queue.mutex: 32 | v = prompt_queue.currently_running[item_id] 33 | if abs(v[0]) == PromptQueueTracker.tracked_id: 34 | PromptQueueTracker.done_event.set() 35 | 36 | return original_task_done(item_id, output, *args, **kwargs) 37 | 38 | prompt_queue.task_done = functools.partial(patched_task_done, original_task_done=prompt_queue.task_done) 39 | 40 | # wipe_queue 41 | def patched_wipe_queue(*args, original_wipe_queue, **kwargs): 42 | with prompt_queue.mutex: 43 | should_release_webui = True 44 | for v in prompt_queue.currently_running.values(): 45 | if abs(v[0]) == PromptQueueTracker.tracked_id: 46 | should_release_webui = False 47 | 48 | if should_release_webui: 49 | PromptQueueTracker.done_event.set() 50 | 51 | return original_wipe_queue(*args, **kwargs) 52 | 53 | prompt_queue.wipe_queue = functools.partial(patched_wipe_queue, original_wipe_queue=prompt_queue.wipe_queue) 54 | 55 | # delete_queue_item 56 | def patched_delete_queue_item(function, *args, original_delete_queue_item, **kwargs): 57 | def patched_function(x): 58 | res = function(x) 59 | if res and abs(x[0]) == PromptQueueTracker.tracked_id: 60 | PromptQueueTracker.done_event.set() 61 | return res 62 | 63 | return original_delete_queue_item(patched_function, *args, **kwargs) 64 | 65 | prompt_queue.delete_queue_item = functools.partial(patched_delete_queue_item, original_delete_queue_item=prompt_queue.delete_queue_item) 66 | 67 | 68 | @ipc.run_in_process('comfyui') 69 | def setup_tracker_id(): 70 | PromptQueueTracker.original_id = PromptQueueTracker.tracked_id 71 | PromptQueueTracker.tracked_id = PromptQueueTracker.server_instance.number 72 | PromptQueueTracker.put_event.clear() 73 | PromptQueueTracker.done_event.clear() 74 | 75 | 76 | @ipc.restrict_to_process('webui') 77 | def wait_until_done(): 78 | if not wait_until_put(): 79 | return False 80 | 81 | from modules import shared 82 | while True: 83 | has_been_set = check_done_event(timeout=1) 84 | if has_been_set: 85 | return True 86 | elif not tracked_id_present(): 87 | return False 88 | elif shared.state.interrupted: 89 | cancel_queued_workflow() 90 | return False 91 | 92 | 93 | @ipc.run_in_process('comfyui') 94 | def check_done_event(*args, **kwargs): 95 | return PromptQueueTracker.done_event.wait(*args, **kwargs) 96 | 97 | 98 | @ipc.run_in_process('comfyui') 99 | def wait_until_put(): 100 | was_put = PromptQueueTracker.put_event.wait(timeout=3) 101 | if not was_put: 102 | PromptQueueTracker.tracked_id = PromptQueueTracker.original_id 103 | return False 104 | 105 | return True 106 | 107 | 108 | @ipc.run_in_process('comfyui') 109 | def cancel_queued_workflow(): 110 | with PromptQueueTracker.queue_instance.mutex: 111 | is_running = False 112 | for v in PromptQueueTracker.queue_instance.currently_running.values(): 113 | if abs(v[0]) == PromptQueueTracker.tracked_id: 114 | is_running = True 115 | 116 | if is_running: 117 | import nodes 118 | nodes.interrupt_processing() 119 | return 120 | 121 | PromptQueueTracker.queue_instance.delete_queue_item(lambda a: a[1] == PromptQueueTracker.tracked_id) 122 | 123 | 124 | @ipc.run_in_process('comfyui') 125 | def tracked_id_present(): 126 | with PromptQueueTracker.queue_instance.mutex: 127 | for v in PromptQueueTracker.queue_instance.currently_running.values(): 128 | if abs(v[0]) == PromptQueueTracker.tracked_id: 129 | return True 130 | for x in PromptQueueTracker.queue_instance.queue: 131 | if abs(x[0]) == PromptQueueTracker.tracked_id: 132 | return True 133 | return False 134 | 135 | 136 | @ipc.restrict_to_process('comfyui') 137 | def add_queue__init__patch(callback): 138 | import execution 139 | original_init = execution.PromptQueue.__init__ 140 | 141 | def patched_PromptQueue__init__(self, server, *args, **kwargs): 142 | original_init(self, server, *args, **kwargs) 143 | callback(self, server, *args, **kwargs) 144 | 145 | execution.PromptQueue.__init__ = patched_PromptQueue__init__ 146 | 147 | 148 | @ipc.restrict_to_process('comfyui') 149 | def patch_prompt_queue(): 150 | add_queue__init__patch(PromptQueueTracker.patched__init__) 151 | -------------------------------------------------------------------------------- /lib_comfyui/comfyui/routes_extension.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from lib_comfyui import external_code 4 | from lib_comfyui.comfyui.iframe_requests import ComfyuiIFrameRequests 5 | 6 | 7 | def patch_server_routes(): 8 | add_server__init__patch(websocket_handler_patch) 9 | add_server__init__patch(workflow_type_ops_server_patch) 10 | 11 | 12 | def add_server__init__patch(callback): 13 | import server 14 | original_init = server.PromptServer.__init__ 15 | 16 | def patched_PromptServer__init__(self, loop: asyncio.AbstractEventLoop, *args, **kwargs): 17 | original_init(self, loop, *args, **kwargs) 18 | callback(self, loop, *args, **kwargs) 19 | 20 | server.PromptServer.__init__ = patched_PromptServer__init__ 21 | 22 | 23 | def websocket_handler_patch(instance, _loop): 24 | from aiohttp import web 25 | 26 | ComfyuiIFrameRequests.server_instance = instance 27 | 28 | @instance.routes.post("/sd-webui-comfyui/webui_register_client") 29 | async def webui_register_client(request): 30 | request = await request.json() 31 | 32 | ComfyuiIFrameRequests.register_client(request) 33 | 34 | return web.json_response() 35 | 36 | @instance.routes.post("/sd-webui-comfyui/webui_ws_response") 37 | async def webui_ws_response(response): 38 | response = await response.json() 39 | 40 | ComfyuiIFrameRequests.handle_response(response['response'] if 'response' in response else response) 41 | 42 | return web.json_response(status=200) 43 | 44 | 45 | def workflow_type_ops_server_patch(instance, _loop): 46 | from aiohttp import web 47 | 48 | @instance.routes.get("/sd-webui-comfyui/workflow_type") 49 | async def get_workflow_type(request): 50 | workflow_type_id = request.rel_url.query.get("workflowTypeId", None) 51 | workflow_type = next(iter( 52 | workflow_type 53 | for workflow_type in external_code.get_workflow_types() 54 | if workflow_type_id in workflow_type.get_ids() 55 | )) 56 | return web.json_response({ 57 | "displayName": workflow_type.display_name, 58 | "webuiIoTypes": { 59 | "inputs": list(workflow_type.input_types) if isinstance(workflow_type.input_types, tuple) else workflow_type.input_types, 60 | "outputs": list(workflow_type.types) if isinstance(workflow_type.types, tuple) else workflow_type.types, 61 | }, 62 | "defaultWorkflow": json.loads(external_code.get_default_workflow_json(workflow_type_id)) 63 | }) 64 | -------------------------------------------------------------------------------- /lib_comfyui/comfyui/type_conversion.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from PIL import Image 3 | import torchvision.transforms.functional as F 4 | from lib_comfyui import ipc 5 | from lib_comfyui.webui.proxies import get_comfy_model_config 6 | 7 | 8 | def webui_image_to_comfyui(batch): 9 | if isinstance(batch[0], Image.Image): 10 | batch = torch.stack([F.pil_to_tensor(image) / 255 for image in batch]) 11 | return batch.permute(0, 2, 3, 1) 12 | 13 | 14 | def comfyui_image_to_webui(batch, return_tensors=False): 15 | batch = batch.permute(0, 3, 1, 2) 16 | if return_tensors: 17 | return batch 18 | 19 | return [F.to_pil_image(image) for image in batch] 20 | 21 | 22 | @ipc.run_in_process('comfyui') 23 | def webui_latent_to_comfyui(batch): 24 | latent_format = get_comfy_model_config().latent_format 25 | return {'samples': latent_format.process_out(batch)} 26 | 27 | 28 | @ipc.run_in_process('comfyui') 29 | def comfyui_latent_to_webui(batch): 30 | latent_format = get_comfy_model_config().latent_format 31 | return latent_format.process_in(batch['samples']) 32 | -------------------------------------------------------------------------------- /lib_comfyui/comfyui_process.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import inspect 3 | import os 4 | import subprocess 5 | import sys 6 | from pathlib import Path 7 | 8 | from lib_comfyui import ipc, torch_utils, argv_conversion, global_state 9 | from lib_comfyui.webui import settings 10 | from lib_comfyui.comfyui import pre_main 11 | 12 | 13 | comfyui_process = None 14 | 15 | 16 | @ipc.restrict_to_process('webui') 17 | def start(): 18 | if not global_state.enabled: 19 | return 20 | 21 | install_location = settings.get_install_location() 22 | if not install_location.exists(): 23 | print('[sd-webui-comfyui]', f'Could not find ComfyUI under directory "{install_location}". The server will NOT be started.', file=sys.stderr) 24 | return 25 | 26 | ipc.current_callback_listeners = {'webui': ipc.callback.CallbackWatcher(ipc.call_fully_qualified, 'webui', global_state.ipc_strategy_class, clear_on_init=True)} 27 | ipc.current_callback_proxies = {'comfyui': ipc.callback.CallbackProxy('comfyui', global_state.ipc_strategy_class, clear_on_init=True)} 28 | ipc.start_callback_listeners() 29 | atexit.register(stop) 30 | start_comfyui_process(install_location) 31 | 32 | 33 | @ipc.restrict_to_process('webui') 34 | def start_comfyui_process(comfyui_install_location): 35 | global comfyui_process 36 | 37 | executable = get_comfyui_executable(comfyui_install_location) 38 | comfyui_env = get_comfyui_env(comfyui_install_location) 39 | install_comfyui_requirements(executable, comfyui_install_location, comfyui_env) 40 | args = [executable, inspect.getfile(pre_main)] + argv_conversion.get_comfyui_args() 41 | 42 | print('[sd-webui-comfyui]', 'Starting subprocess for comfyui...') 43 | comfyui_process = subprocess.Popen( 44 | args=args, 45 | executable=executable, 46 | cwd=str(comfyui_install_location), 47 | env=comfyui_env, 48 | ) 49 | 50 | 51 | def get_comfyui_executable(comfyui_install_location: Path) -> str: 52 | executable = sys.executable 53 | 54 | if os.name == 'nt': 55 | executable_paths = [ 56 | comfyui_install_location / 'venv' / 'scripts' / 'python.exe', 57 | comfyui_install_location.parent / 'python_embeded' / 'python.exe', 58 | ] 59 | else: 60 | executable_paths = [ 61 | comfyui_install_location / 'venv' / 'bin' / 'python', 62 | comfyui_install_location.parent / 'python_embeded' / 'python', 63 | ] 64 | 65 | for potential_executable in executable_paths: 66 | if potential_executable.exists(): 67 | executable = potential_executable 68 | print('[sd-webui-comfyui]', 'Detected custom ComfyUI venv:', executable) 69 | break 70 | 71 | return str(executable) 72 | 73 | 74 | def get_comfyui_env(comfyui_install_location): 75 | comfyui_env = os.environ.copy() 76 | if 'PYTHONPATH' in comfyui_env: 77 | del comfyui_env['PYTHONPATH'] 78 | 79 | problematic_windows_tmp_dirs = { 80 | k: v 81 | for k, v in comfyui_env.items() 82 | if k in ("TMP", "TEMP") and v == "tmp" 83 | } 84 | if os.name == "nt" and problematic_windows_tmp_dirs: 85 | problematic_windows_tmp_dirs = " and ".join(f'{k}="{v}"' for k, v in problematic_windows_tmp_dirs.items()) 86 | message_prefix = "[sd-webui-comfyui] " 87 | print( 88 | "\n".join([ 89 | f'{message_prefix}Found {problematic_windows_tmp_dirs} in the environment. On Windows, this is known to cause issues.', 90 | f'{message_prefix}If ComfyUI refuses to start, consider using TEMP="temp" or TMP="temp" as a workaround.', 91 | f'{message_prefix}For more information, see https://github.com/ModelSurge/sd-webui-comfyui/issues/170#issuecomment-1792009789', 92 | ]), 93 | file=sys.stderr, 94 | ) 95 | 96 | comfyui_env['SD_WEBUI_COMFYUI_EXTENSION_DIR'] = settings.get_extension_base_dir() 97 | comfyui_env['SD_WEBUI_COMFYUI_IPC_STRATEGY_CLASS_NAME'] = global_state.ipc_strategy_class.__name__ 98 | return comfyui_env 99 | 100 | 101 | def install_comfyui_requirements(executable, comfyui_install_location, comfyui_env): 102 | if executable == sys.executable: 103 | # requirements already installed in the webui by install.py 104 | return 105 | 106 | print('[sd-webui-comfyui]', 'Installing mandatory pip requirements in ComfyUI venv...') 107 | subprocess.check_call( 108 | args=[ 109 | executable, 110 | *(['-s'] if "python_embeded" in executable or "python_embedded" in executable else []), 111 | '-m', 112 | 'pip', 113 | 'install', 114 | '-r', 115 | str(Path(settings.get_extension_base_dir(), 'requirements.txt')), 116 | ], 117 | executable=executable, 118 | cwd=str(comfyui_install_location), 119 | env=comfyui_env, 120 | ) 121 | 122 | 123 | @ipc.restrict_to_process('webui') 124 | def stop(): 125 | atexit.unregister(stop) 126 | stop_comfyui_process() 127 | ipc.stop_callback_listeners() 128 | 129 | 130 | @ipc.restrict_to_process('webui') 131 | def stop_comfyui_process(): 132 | global comfyui_process 133 | if comfyui_process is None: 134 | return 135 | 136 | print('[sd-webui-comfyui]', 'Attempting to gracefully terminate the ComfyUI server...') 137 | comfyui_process.terminate() 138 | try: 139 | comfyui_process.wait(global_state.comfyui_graceful_termination_timeout) 140 | print('[sd-webui-comfyui]', 'The ComfyUI server was gracefully terminated') 141 | except subprocess.TimeoutExpired: 142 | print('[sd-webui-comfyui]', 'Graceful termination timed out. Killing the ComfyUI server...') 143 | comfyui_process.kill() 144 | print('[sd-webui-comfyui]', 'The ComfyUI server was killed') 145 | comfyui_process = None 146 | -------------------------------------------------------------------------------- /lib_comfyui/custom_extension_injector.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lib_comfyui import find_extensions, ipc 3 | 4 | 5 | def register_webui_extensions(): 6 | node_paths, script_paths = find_extensions.get_extension_paths_to_load() 7 | register_custom_nodes(node_paths) 8 | register_custom_scripts(script_paths) 9 | 10 | 11 | @ipc.restrict_to_process('comfyui') 12 | def register_custom_nodes(custom_nodes_path_list): 13 | from folder_paths import add_model_folder_path 14 | 15 | for custom_nodes_path in custom_nodes_path_list: 16 | add_model_folder_path('custom_nodes', custom_nodes_path) 17 | 18 | 19 | @ipc.restrict_to_process('comfyui') 20 | def register_custom_scripts(custom_scripts_path_list): 21 | from nodes import EXTENSION_WEB_DIRS 22 | 23 | for custom_scripts_path in custom_scripts_path_list: 24 | name = f"webui_scripts/{os.path.basename(os.path.dirname(custom_scripts_path))}" 25 | dir = custom_scripts_path 26 | EXTENSION_WEB_DIRS[name] = dir 27 | -------------------------------------------------------------------------------- /lib_comfyui/default_workflow_types.py: -------------------------------------------------------------------------------- 1 | from lib_comfyui import external_code 2 | 3 | 4 | sandbox_tab_workflow_type = external_code.WorkflowType( 5 | base_id='sandbox', 6 | display_name='ComfyUI', 7 | tabs='tab', 8 | ) 9 | preprocess_workflow_type = external_code.WorkflowType( 10 | base_id='preprocess', 11 | display_name='Preprocess', 12 | tabs='img2img', 13 | default_workflow=external_code.AUTO_WORKFLOW, 14 | types='IMAGE', 15 | ) 16 | preprocess_latent_workflow_type = external_code.WorkflowType( 17 | base_id='preprocess_latent', 18 | display_name='Preprocess (latent)', 19 | tabs='img2img', 20 | default_workflow=external_code.AUTO_WORKFLOW, 21 | types='LATENT', 22 | ) 23 | postprocess_latent_workflow_type = external_code.WorkflowType( 24 | base_id='postprocess_latent', 25 | display_name='Postprocess (latent)', 26 | default_workflow=external_code.AUTO_WORKFLOW, 27 | types='LATENT', 28 | ) 29 | postprocess_workflow_type = external_code.WorkflowType( 30 | base_id='postprocess', 31 | display_name='Postprocess', 32 | default_workflow=external_code.AUTO_WORKFLOW, 33 | types='IMAGE', 34 | ) 35 | postprocess_image_workflow_type = external_code.WorkflowType( 36 | base_id='postprocess_image', 37 | display_name='Postprocess image', 38 | default_workflow=external_code.AUTO_WORKFLOW, 39 | types='IMAGE', 40 | max_amount_of_ToWebui_nodes=1, 41 | ) 42 | before_save_image_workflow_type = external_code.WorkflowType( 43 | base_id='before_save_image', 44 | display_name='Before save image', 45 | default_workflow=external_code.AUTO_WORKFLOW, 46 | types='IMAGE', 47 | max_amount_of_ToWebui_nodes=1, 48 | ) 49 | 50 | 51 | def add_default_workflow_types(): 52 | workflow_types = [ 53 | sandbox_tab_workflow_type, 54 | preprocess_workflow_type, 55 | preprocess_latent_workflow_type, 56 | postprocess_latent_workflow_type, 57 | postprocess_workflow_type, 58 | postprocess_image_workflow_type, 59 | before_save_image_workflow_type, 60 | ] 61 | 62 | for workflow_type in workflow_types: 63 | external_code.add_workflow_type(workflow_type) 64 | -------------------------------------------------------------------------------- /lib_comfyui/external_code/__init__.py: -------------------------------------------------------------------------------- 1 | def fix_path(): 2 | import sys 3 | from pathlib import Path 4 | 5 | extension_path = str(Path(__file__).parent.parent.parent) 6 | if extension_path not in sys.path: 7 | sys.path.append(extension_path) 8 | 9 | 10 | fix_path() 11 | from .api import * 12 | -------------------------------------------------------------------------------- /lib_comfyui/external_code/api.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import sys 3 | import traceback 4 | from pathlib import Path 5 | from typing import List, Tuple, Union, Any, Optional, Dict 6 | from lib_comfyui import global_state, ipc 7 | 8 | 9 | ALL_TABS = ... 10 | Tabs = Union[str, Tuple[str, ...]] 11 | AUTO_WORKFLOW = '"auto"' # json encoded string 12 | 13 | 14 | @dataclasses.dataclass 15 | class WorkflowType: 16 | """ 17 | Describes a unique type of ComfyUI workflow 18 | """ 19 | 20 | base_id: str 21 | display_name: str 22 | tabs: Tabs = ('txt2img', 'img2img') 23 | default_workflow: Union[str, Path] = "null" 24 | types: Union[str, Tuple[str, ...], Dict[str, str]] = dataclasses.field(default_factory=tuple) 25 | input_types: Union[str, Tuple[str, ...], Dict[str, str], None] = None 26 | max_amount_of_ToWebui_nodes: Optional[int] = None 27 | max_amount_of_FromWebui_nodes: Optional[int] = None 28 | 29 | def __post_init__(self): 30 | if isinstance(self.tabs, str): 31 | self.tabs = (self.tabs,) 32 | 33 | if self.input_types is None: 34 | self.input_types = self.types 35 | 36 | if not isinstance(self.types, (str, tuple, dict)): 37 | raise TypeError(f'types should be str, tuple or dict but it is {type(self.types)}') 38 | 39 | if not isinstance(self.input_types, (str, tuple, dict)): 40 | raise TypeError(f'input_types should be str, tuple or dict but it is {type(self.input_types)}') 41 | 42 | assert self.tabs, "tabs must not be empty" 43 | 44 | if isinstance(self.default_workflow, Path): 45 | with open(str(self.default_workflow), 'r') as f: 46 | self.default_workflow = f.read() 47 | elif self.default_workflow == AUTO_WORKFLOW: 48 | if not self.is_same_io(): 49 | raise ValueError('AUTO_WORKFLOW default workflow is only supported for same input and output types') 50 | 51 | def get_ids(self, tabs: Tabs = ALL_TABS) -> List[str]: 52 | if isinstance(tabs, str): 53 | tabs = (tabs,) 54 | 55 | return [ 56 | f'{self.base_id}_{tab}' 57 | for tab in self.tabs 58 | if tabs == ALL_TABS or tab in tabs 59 | ] 60 | 61 | def pretty_str(self): 62 | return f'"{self.display_name}" ({self.base_id})' 63 | 64 | def is_same_io(self): 65 | def normalize_to_tuple(types): 66 | if isinstance(types, dict): 67 | return tuple(types.values()) 68 | elif isinstance(types, str): 69 | return types, 70 | return types 71 | 72 | return normalize_to_tuple(self.input_types) == normalize_to_tuple(self.types) 73 | 74 | 75 | def get_workflow_types(tabs: Tabs = ALL_TABS) -> List[WorkflowType]: 76 | """ 77 | Get the list of currently registered workflow types 78 | To update the workflow types list, see `add_workflow_type` or `set_workflow_types` 79 | 80 | Args: 81 | tabs (Tabs): Whitelist of tabs 82 | Returns: 83 | List of workflow types that are defined on at least one of the given tabs 84 | """ 85 | if isinstance(tabs, str): 86 | tabs = (tabs,) 87 | 88 | return [ 89 | workflow_type 90 | for workflow_type in getattr(global_state, 'workflow_types', []) 91 | if tabs == ALL_TABS or any(tab in tabs for tab in workflow_type.tabs) 92 | ] 93 | 94 | 95 | def add_workflow_type(new_workflow_type: WorkflowType) -> None: 96 | """ 97 | Register a new workflow type 98 | You cannot call this function after the extension ui has been created 99 | 100 | Args: 101 | new_workflow_type (WorkflowType): new workflow type to add 102 | Raises: 103 | ValueError: If the id or display_name of new_workflow_type has already been registered 104 | NotImplementedError: if the workflow types list is modified after the ui has been instantiated 105 | """ 106 | workflow_types = get_workflow_types() 107 | 108 | for existing_workflow_type in workflow_types: 109 | if existing_workflow_type.base_id == new_workflow_type.base_id: 110 | raise ValueError(f'The id {new_workflow_type.base_id} already exists') 111 | if existing_workflow_type.display_name == new_workflow_type.display_name: 112 | raise ValueError(f'The display name {new_workflow_type.display_name} is already in use by workflow type {existing_workflow_type.base_id}') 113 | 114 | if getattr(global_state, 'is_ui_instantiated', False): 115 | raise NotImplementedError('Cannot modify workflow types after the ui has been instantiated') 116 | 117 | workflow_types.append(new_workflow_type) 118 | set_workflow_types(workflow_types) 119 | 120 | 121 | def set_workflow_types(workflow_types: List[WorkflowType]) -> None: 122 | """ 123 | Set the list of currently registered workflow types 124 | You cannot call this function after the extension ui has been created 125 | Args: 126 | workflow_types (List[WorkflowType]): the new workflow types 127 | Raises: 128 | NotImplementedError: if the workflow types list is modified after the ui has been instantiated 129 | Notes: 130 | A deep copy of workflow_types is used when calling this function from the comfyui process 131 | No copy is made when calling this function from the webui process 132 | """ 133 | if getattr(global_state, 'is_ui_instantiated', False): 134 | raise NotImplementedError('Cannot modify workflow types after the ui has been instantiated') 135 | 136 | global_state.workflow_types = workflow_types 137 | 138 | 139 | def clear_workflow_types() -> None: 140 | """ 141 | Clear the list of currently registered workflow types 142 | You cannot call this function after the extension ui has been created 143 | Raises: 144 | NotImplementedError: if the workflow types list is modified after the ui has been instantiated 145 | """ 146 | set_workflow_types([]) 147 | 148 | 149 | def get_workflow_type_ids(tabs: Tabs = ALL_TABS) -> List[str]: 150 | """ 151 | Get all workflow type ids of all currently registered workflow types 152 | Multiple ids can be assigned to each workflow type depending on how many tabs it is to be displayed on 153 | 154 | Args: 155 | tabs (Tabs): Whitelist of tabs for which to return the ids 156 | Returns: 157 | List of ids for the given tabs 158 | """ 159 | res = [] 160 | 161 | for workflow_type in get_workflow_types(tabs): 162 | res.extend(workflow_type.get_ids(tabs)) 163 | 164 | return res 165 | 166 | 167 | def get_workflow_type_display_names(tabs: Tabs = ALL_TABS) -> List[str]: 168 | """ 169 | Get the list of display names needed for the given tabs 170 | 171 | Args: 172 | tabs (Tabs): Whitelist of tabs for which to return the display names 173 | Returns: 174 | List of display names for the given tabs 175 | """ 176 | return [workflow_type.display_name for workflow_type in get_workflow_types(tabs)] 177 | 178 | 179 | def get_default_workflow_json(workflow_type_id: str) -> str: 180 | """ 181 | Get the default workflow for the given workflow type id 182 | 183 | Args: 184 | workflow_type_id (str): The workflow type id for which to get the default workflow 185 | Returns: 186 | The default workflow, or None if there is no default workflow for the given workflow type 187 | Raises: 188 | ValueError: If workflow_type_id does not exist 189 | """ 190 | for workflow_type in get_workflow_types(): 191 | if workflow_type_id in workflow_type.get_ids(): 192 | return workflow_type.default_workflow 193 | 194 | raise ValueError(workflow_type_id) 195 | 196 | 197 | def is_workflow_type_enabled(workflow_type_id: str) -> bool: 198 | return ( 199 | getattr(global_state, 'enable', True) and 200 | getattr(global_state, 'enabled_workflow_type_ids', {}).get(workflow_type_id, False) 201 | ) 202 | 203 | 204 | @ipc.restrict_to_process('webui') 205 | def run_workflow( 206 | workflow_type: WorkflowType, 207 | tab: str, 208 | batch_input: Any, 209 | queue_front: Optional[bool] = None, 210 | identity_on_error: Optional[bool] = False 211 | ) -> List[Any]: 212 | """ 213 | Run a comfyui workflow synchronously 214 | 215 | Args: 216 | workflow_type (WorkflowType): Target workflow type to run 217 | tab (str): The tab on which to run the workflow type. The workflow type must be present on the tab 218 | batch_input (Any): Batch object to pass as input to the workflow. The number of elements in this batch object will be the size of the comfyui batch. 219 | The particular type of batch_input depends on **workflow_type.input_types**: 220 | - if **input_types** is a dict, **batch_input** should be a dict. For each **k, v** pair of **batch_input**, **v** should match the type expected by **input_types[k]**. 221 | - if **input_types** is a tuple, **batch_input** should be a tuple. Each element **v** at index **i** of **batch_input** should match the type expected by **input_types[i]**. 222 | - if **input_types** is a str, **batch_input** should be a single value that should match the type expected by **input_types**. 223 | queue_front (Optional[bool]): Whether to queue the workflow before or after the currently queued workflows 224 | identity_on_error (Optional[bool]): Whether to return batch_input (converted to the type expected by workflow_type.types) instead of raising a RuntimeError when the workflow fails to run 225 | Returns: 226 | The outputs of the workflow, as a list. 227 | Each element of the list corresponds to one output node in the workflow. 228 | You can expect multiple values when a user has multiple output nodes in their workflow. 229 | The particular type of each returned element depends on workflow_type.types: 230 | - **types** is a dict: each element is a dict. For each **k, v** pair, **v** matches the type expected by **types[k]** 231 | - **types** is a tuple: each element is a tuple. For each element **v** at index **i**, **v** matches the type expected by **types[i]** 232 | - **types** is a str: each element is a single value that matches the type expected by **types** 233 | Each element of the list will have the same batch size as **batch_input** 234 | Raises: 235 | ValueError: If workflow_type is not present on the given tab 236 | TypeError: If the type of batch_input does not match the type expected by workflow_type.input_types 237 | RuntimeError: If identity_on_error is False and workflow execution fails for any reason 238 | AssertionError: If multiple candidate ids exist for workflow_type 239 | """ 240 | from lib_comfyui.comfyui.iframe_requests import ComfyuiIFrameRequests 241 | 242 | candidate_ids = workflow_type.get_ids(tab) 243 | assert len(candidate_ids) <= 1, ( 244 | f'Found multiple candidate workflow type ids for tab {tab} and workflow type {workflow_type.pretty_str()}: {candidate_ids}\n' 245 | 'The workflow type is likely to be incorrectly configured' 246 | ) 247 | 248 | if queue_front is None: 249 | queue_front = getattr(global_state, 'queue_front', True) 250 | 251 | batch_input_args, input_types = _normalize_to_tuple(batch_input, workflow_type.input_types) 252 | 253 | if not candidate_ids: 254 | raise ValueError(f'The workflow type {workflow_type.pretty_str()} does not exist on tab {tab}. ' 255 | f'Valid tabs for the given workflow type are: {workflow_type.tabs}') 256 | 257 | workflow_type_id = candidate_ids[0] 258 | 259 | try: 260 | ComfyuiIFrameRequests.validate_amount_of_nodes_or_throw( 261 | workflow_type_id, 262 | workflow_type.max_amount_of_FromWebui_nodes, 263 | workflow_type.max_amount_of_ToWebui_nodes, 264 | ) 265 | except RuntimeError as e: 266 | if not identity_on_error: 267 | raise e 268 | 269 | traceback.print_exception(e) 270 | return batch_input_args 271 | 272 | try: 273 | if not is_workflow_type_enabled(workflow_type_id): 274 | raise WorkflowTypeDisabled(f'Workflow type {workflow_type.pretty_str()} is not enabled on tab {tab}') 275 | 276 | batch_output_params = ComfyuiIFrameRequests.start_workflow_sync( 277 | batch_input_args=batch_input_args, 278 | workflow_type_id=workflow_type_id, 279 | workflow_input_types=input_types, 280 | queue_front=queue_front, 281 | ) 282 | except RuntimeError as e: 283 | if not identity_on_error: 284 | raise e 285 | 286 | # don't print just because the workflow type is disabled 287 | if not isinstance(e, WorkflowTypeDisabled): 288 | traceback.print_exception(e) 289 | 290 | if not workflow_type.is_same_io(): 291 | print('[sd-webui-comfyui]', f'Returning input of type {workflow_type.input_types}, which likely does not match the expected output type {workflow_type.types}', file=sys.stderr) 292 | 293 | # denormalize tuple -> tuple|str|dict 294 | if isinstance(workflow_type.types, tuple): 295 | return [batch_input_args] 296 | elif isinstance(workflow_type.types, str): 297 | return [batch_input_args[0]] 298 | else: 299 | return [dict(zip(workflow_type.types.keys(), batch_input_args))] 300 | 301 | # denormalize dict -> tuple|str|dict 302 | if isinstance(workflow_type.types, tuple): 303 | return [tuple(params.values()) for params in batch_output_params] 304 | elif isinstance(workflow_type.types, str): 305 | return [next(iter(params.values())) for params in batch_output_params] 306 | else: 307 | return batch_output_params 308 | 309 | 310 | class WorkflowTypeDisabled(RuntimeError): 311 | pass 312 | 313 | 314 | def _normalize_to_tuple(batch_input, input_types): 315 | if isinstance(input_types, str): 316 | return (batch_input,), (input_types,) 317 | elif isinstance(input_types, tuple): 318 | if not isinstance(batch_input, tuple): 319 | raise TypeError(f'batch_input should be tuple but is instead {type(batch_input)}') 320 | 321 | if len(batch_input) != len(input_types): 322 | raise TypeError( 323 | f'batch_input received {len(batch_input)} values instead of {len(input_types)} (signature is {input_types})') 324 | 325 | return batch_input, input_types 326 | elif isinstance(input_types, dict): 327 | if not isinstance(batch_input, dict): 328 | raise TypeError(f'batch_input should be dict but is instead {type(batch_input)}') 329 | 330 | expected_keys = set(input_types.keys()) 331 | actual_keys = set(batch_input.keys()) 332 | if expected_keys - actual_keys: 333 | raise TypeError(f'batch_input is missing keys: {expected_keys - actual_keys}') 334 | 335 | # convert to tuple in the same order as the items in input_types 336 | return ( 337 | tuple(batch_input[k] for k in input_types.keys()), 338 | tuple(input_types.values()) 339 | ) 340 | else: 341 | raise TypeError(f'batch_input should be str, tuple or dict but is instead {type(batch_input)}') 342 | -------------------------------------------------------------------------------- /lib_comfyui/find_extensions.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from lib_comfyui import ipc 3 | 4 | 5 | @ipc.run_in_process('webui') 6 | def get_extension_paths_to_load(): 7 | from modules.extensions import list_extensions, active 8 | list_extensions() 9 | active_paths = [e.path for e in active()] 10 | root_node_paths = [] 11 | root_script_paths = [] 12 | 13 | for path in active_paths: 14 | root_nodes = os.path.join(path, 'comfyui_custom_nodes') 15 | root_scripts = os.path.join(path, 'comfyui_custom_scripts') 16 | if os.path.exists(root_nodes): 17 | root_node_paths.append(root_nodes) 18 | if os.path.exists(root_scripts): 19 | root_script_paths.append(root_scripts) 20 | 21 | return root_node_paths, root_script_paths 22 | -------------------------------------------------------------------------------- /lib_comfyui/global_state.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from types import ModuleType 3 | from typing import List, Tuple, Optional, Dict, Any 4 | from lib_comfyui import ipc 5 | 6 | 7 | enabled: bool 8 | is_ui_instantiated: bool 9 | queue_front: bool 10 | focused_webui_client_id: Optional[str] = None 11 | 12 | workflow_types: List 13 | enabled_workflow_type_ids: Dict[str, bool] 14 | node_inputs: Tuple[Any, ...] 15 | node_outputs: List[Dict[str, Any]] 16 | current_workflow_input_types: Tuple[str, ...] 17 | 18 | ipc_strategy_class: type 19 | ipc_strategy_class_name: str 20 | comfyui_graceful_termination_timeout: float 21 | 22 | reverse_proxy_enabled: bool 23 | 24 | last_positive_prompt: str 25 | last_negative_prompt: str 26 | 27 | 28 | class GlobalState(ModuleType): 29 | __state = {} 30 | 31 | def __init__(self, glob): 32 | super().__init__(__name__) 33 | for k, v in glob.items(): 34 | setattr(self, k, v) 35 | 36 | def __getattr__(self, item): 37 | if item in ['__file__']: 38 | return globals()[item] 39 | 40 | return GlobalState.getattr(item) 41 | 42 | @staticmethod 43 | @ipc.run_in_process('webui') 44 | def getattr(item): 45 | try: 46 | return GlobalState.__state[item] 47 | except KeyError: 48 | raise AttributeError 49 | 50 | def __setattr__(self, item, value): 51 | GlobalState.setattr(item, value) 52 | 53 | @staticmethod 54 | @ipc.run_in_process('webui') 55 | def setattr(item, value): 56 | GlobalState.__state[item] = value 57 | 58 | def __delattr__(self, item): 59 | GlobalState.delattr(item) 60 | 61 | @staticmethod 62 | @ipc.run_in_process('webui') 63 | def delattr(item): 64 | del GlobalState.__state[item] 65 | 66 | def __contains__(self, item): 67 | return GlobalState.contains(item) 68 | 69 | @staticmethod 70 | @ipc.run_in_process('webui') 71 | def contains(item): 72 | return item in GlobalState.__state 73 | 74 | 75 | sys.modules[__name__] = GlobalState(globals()) 76 | -------------------------------------------------------------------------------- /lib_comfyui/ipc/__init__.py: -------------------------------------------------------------------------------- 1 | from . import callback 2 | from . import payload 3 | from . import strategies 4 | 5 | import gc 6 | import importlib 7 | import sys 8 | import time 9 | import logging 10 | 11 | 12 | def run_in_process(process_id): 13 | def annotation(function): 14 | def wrapper(*args, **kwargs): 15 | global current_process_id 16 | if process_id == current_process_id: 17 | return function(*args, **kwargs) 18 | else: 19 | start = time.time() 20 | res = current_callback_proxies[process_id].get(args=(function.__module__, function.__qualname__, args, kwargs)) 21 | logging.debug( 22 | '[sd-webui-comfyui] IPC call %s -> %s %s:\t%s', 23 | current_process_id, process_id, 24 | time.time() - start, 25 | f'{function.__module__}.{function.__qualname__}(*{args}, **{kwargs})' 26 | ) 27 | return res 28 | 29 | return wrapper 30 | 31 | return annotation 32 | 33 | 34 | def restrict_to_process(process_id): 35 | def annotation(function): 36 | def wrapper(*args, **kwargs): 37 | global current_process_id 38 | if process_id != current_process_id: 39 | raise RuntimeError(f'Can only call function {function.__module__}.{function.__qualname__} from process {process_id}. Current process is {current_process_id}') 40 | 41 | return function(*args, **kwargs) 42 | 43 | return wrapper 44 | 45 | return annotation 46 | 47 | 48 | def call_fully_qualified(module_name, qualified_name, args, kwargs): 49 | module_parts = module_name.split('.') 50 | try: 51 | module = sys.modules[module_parts[0]] 52 | for part in module_parts[1:]: 53 | module = getattr(module, part) 54 | except (AttributeError, KeyError): 55 | source_module = module_parts[-1] 56 | module = importlib.import_module(module_name, source_module) 57 | 58 | function = module 59 | for name in qualified_name.split('.'): 60 | function = getattr(function, name) 61 | return function(*args, **kwargs) 62 | 63 | 64 | current_process_id = 'webui' 65 | current_callback_listeners = {} 66 | current_callback_proxies = {} 67 | 68 | 69 | def start_callback_listeners(): 70 | assert not callback_listeners_started() 71 | for callback_listener in current_callback_listeners.values(): 72 | callback_listener.start() 73 | 74 | print('[sd-webui-comfyui]', 'Started callback listeners for process', current_process_id) 75 | 76 | 77 | def stop_callback_listeners(): 78 | assert callback_listeners_started() 79 | for callback_listener in current_callback_listeners.values(): 80 | callback_listener.stop() 81 | 82 | current_callback_proxies.clear() 83 | current_callback_listeners.clear() 84 | gc.collect() 85 | 86 | print('[sd-webui-comfyui]', 'Stopped callback listeners for process', current_process_id) 87 | 88 | 89 | def callback_listeners_started(): 90 | return any(callback_listener.is_running() for callback_listener in current_callback_listeners.values()) 91 | -------------------------------------------------------------------------------- /lib_comfyui/ipc/callback.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Callable 3 | from lib_comfyui.ipc.payload import IpcSender, IpcReceiver 4 | from lib_comfyui.ipc.strategies import IpcStrategy 5 | 6 | 7 | class CallbackWatcher: 8 | def __init__(self, callback: Callable, name: str, strategy_factory: Callable[[str], IpcStrategy], clear_on_init: bool = False, clear_on_del: bool = True): 9 | self._callback = callback 10 | self._res_sender = IpcSender( 11 | f'res_{name}', 12 | strategy_factory, 13 | clear_on_init=clear_on_init, 14 | clear_on_del=clear_on_del, 15 | ) 16 | self._args_receiver = IpcReceiver( 17 | f'args_{name}', 18 | strategy_factory, 19 | clear_on_init=clear_on_init, 20 | clear_on_del=clear_on_del, 21 | ) 22 | self._producer_thread = None 23 | 24 | def start(self): 25 | def thread_loop(): 26 | while self._producer_thread.is_running(): 27 | self.attend_consumer(timeout=0.5) 28 | 29 | self._producer_thread = StoppableThread(target=thread_loop, daemon=True) 30 | self._producer_thread.start() 31 | 32 | def stop(self): 33 | if self._producer_thread is None: 34 | return 35 | 36 | self._producer_thread.join() 37 | self._producer_thread = None 38 | 39 | def is_running(self): 40 | return self._producer_thread and self._producer_thread.is_running() 41 | 42 | def attend_consumer(self, timeout: float = None): 43 | try: 44 | args, kwargs = self._args_receiver.recv(timeout=timeout) 45 | except TimeoutError: 46 | return 47 | 48 | try: 49 | self._res_sender.send(self._callback(*args, **kwargs)) 50 | except Exception as e: 51 | self._res_sender.send(RemoteError(e)) 52 | 53 | 54 | class CallbackProxy: 55 | def __init__(self, name: str, strategy_factory, clear_on_init: bool = False, clear_on_del: bool = True): 56 | self._res_receiver = IpcReceiver( 57 | f'res_{name}', 58 | strategy_factory, 59 | clear_on_init=clear_on_init, 60 | clear_on_del=clear_on_del, 61 | ) 62 | self._args_sender = IpcSender( 63 | f'args_{name}', 64 | strategy_factory, 65 | clear_on_init=clear_on_init, 66 | clear_on_del=clear_on_del, 67 | ) 68 | 69 | def get(self, args=None, kwargs=None): 70 | self._args_sender.send((args if args is not None else (), kwargs if kwargs is not None else {})) 71 | res = self._res_receiver.recv() 72 | if isinstance(res, RemoteError): 73 | raise res.error from res 74 | else: 75 | return res 76 | 77 | 78 | class RemoteError(Exception): 79 | def __init__(self, error): 80 | self.error = error 81 | 82 | 83 | class StoppableThread(threading.Thread): 84 | def __init__(self, *args, **kwargs): 85 | super().__init__(*args, **kwargs) 86 | self._stop_event = threading.Event() 87 | 88 | def stop(self): 89 | self._stop_event.set() 90 | 91 | def join(self, *args, **kwargs) -> None: 92 | self.stop() 93 | super().join(*args, **kwargs) 94 | 95 | def is_running(self): 96 | return not self._stop_event.is_set() 97 | -------------------------------------------------------------------------------- /lib_comfyui/ipc/payload.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import math 3 | import pickle 4 | import portalocker 5 | import tempfile 6 | import time 7 | import logging 8 | from pathlib import Path 9 | from typing import Optional, Any, Callable 10 | from lib_comfyui.ipc.strategies import IpcStrategy 11 | 12 | 13 | class IpcPayload: 14 | def __init__(self, name, strategy_factory: Callable[[str], IpcStrategy], lock_directory: str = None, clear_on_init: bool = False, clear_on_del: bool = True): 15 | self._name = name 16 | self._strategy = strategy_factory(f'ipc_payload_{name}') 17 | lock_directory = tempfile.gettempdir() if lock_directory is None else lock_directory 18 | self._lock_path = Path(lock_directory, f'ipc_payload_{name}') 19 | self._clear_on_del = clear_on_del 20 | 21 | if clear_on_init: 22 | with self.get_lock() as lock_file: 23 | self._strategy.clear(lock_file) 24 | 25 | def __del__(self): 26 | if self._clear_on_del: 27 | lock = self.get_lock() 28 | with lock as lock_file: 29 | self._strategy.clear(lock_file) 30 | 31 | def get_lock(self, timeout: Optional[float] = None, mode: str = 'wb+'): 32 | return portalocker.Lock( 33 | self._lock_path, 34 | mode=mode, 35 | timeout=timeout, 36 | check_interval=0.02, 37 | flags=portalocker.LOCK_EX | (portalocker.LOCK_NB * int(timeout is not None)), 38 | ) 39 | 40 | 41 | class IpcSender(IpcPayload): 42 | def send(self, value: Any): 43 | with self.get_lock() as lock_file: 44 | logging.debug(f'IPC payload {self._name}\tsend value: {value}') 45 | self._strategy.set_data(lock_file, pickle.dumps(value)) 46 | 47 | 48 | class IpcReceiver(IpcPayload): 49 | def recv(self, timeout: Optional[float] = None) -> Any: 50 | current_time = time.time() 51 | end_time = (current_time + timeout) if timeout is not None else math.inf 52 | 53 | while current_time < end_time: 54 | lock = self.get_lock(timeout=end_time - current_time, mode='rb+') 55 | 56 | try: 57 | with lock as lock_file: 58 | if self._strategy.is_empty(lock_file): 59 | raise FileEmptyException 60 | 61 | with self._strategy.get_data(lock_file) as data, restore_torch_load(): 62 | value = pickle.loads(data) 63 | del data 64 | 65 | logging.debug(f'IPC payload {self._name}\treceive value: {value}') 66 | return value 67 | except FileEmptyException: 68 | time.sleep(lock.check_interval) # yuck 69 | current_time = time.time() 70 | continue 71 | except portalocker.LockException: 72 | break 73 | 74 | raise TimeoutError 75 | 76 | 77 | class FileEmptyException(Exception): 78 | pass 79 | 80 | 81 | @contextlib.contextmanager 82 | def restore_torch_load(): 83 | from lib_comfyui import ipc 84 | import torch 85 | original_torch_load = torch.load 86 | 87 | if ipc.current_process_id == 'webui': 88 | try: 89 | from modules import safe 90 | torch.load = safe.unsafe_torch_load 91 | del safe 92 | except ImportError: 93 | pass 94 | 95 | yield 96 | 97 | torch.load = original_torch_load 98 | -------------------------------------------------------------------------------- /lib_comfyui/ipc/strategies.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import dataclasses 3 | import os 4 | import pickle 5 | import platform 6 | from abc import ABC, abstractmethod 7 | from multiprocessing.shared_memory import SharedMemory 8 | from typing import IO, Union 9 | 10 | 11 | BytesLike = Union[bytes, bytearray, memoryview] 12 | 13 | 14 | __all__ = [ 15 | 'FileSystemIpcStrategy', 16 | 'SharedMemoryIpcStrategy', 17 | 'OsFriendlyIpcStrategy', 18 | ] 19 | 20 | 21 | class IpcStrategy(ABC): 22 | @abstractmethod 23 | def is_empty(self, lock_file: IO) -> bool: 24 | pass 25 | 26 | @abstractmethod 27 | def set_data(self, lock_file: IO, data: BytesLike) -> None: 28 | pass 29 | 30 | @abstractmethod 31 | @contextlib.contextmanager 32 | def get_data(self, lock_file: IO) -> BytesLike: 33 | pass 34 | 35 | @abstractmethod 36 | def clear(self, lock_file: IO) -> None: 37 | pass 38 | 39 | 40 | class FileSystemIpcStrategy(IpcStrategy): 41 | def __init__(self, _name: str): 42 | pass 43 | 44 | def is_empty(self, lock_file: IO) -> bool: 45 | lock_file.seek(0, os.SEEK_END) 46 | return lock_file.tell() == 0 47 | 48 | def set_data(self, lock_file: IO, data: BytesLike): 49 | lock_file.write(data) 50 | 51 | @contextlib.contextmanager 52 | def get_data(self, lock_file: IO) -> BytesLike: 53 | lock_file.seek(0) 54 | yield lock_file.read() 55 | self.clear(lock_file) 56 | 57 | def clear(self, lock_file: IO): 58 | lock_file.seek(0) 59 | lock_file.truncate() 60 | 61 | 62 | class SharedMemoryIpcStrategy(IpcStrategy): 63 | def __init__(self, shm_name: str): 64 | self._shm_name = shm_name 65 | self._shm = None 66 | 67 | @dataclasses.dataclass 68 | class Metadata: 69 | is_empty: bool 70 | size: int 71 | 72 | def _get_metadata(self, lock_file: IO) -> Metadata: 73 | file_size = lock_file.seek(0, os.SEEK_END) 74 | if file_size == 0: 75 | return self.Metadata(is_empty=True, size=0) 76 | 77 | lock_file.seek(0) 78 | try: 79 | return pickle.loads(lock_file.read()) 80 | except (ModuleNotFoundError, AttributeError): 81 | return self.Metadata(is_empty=True, size=0) 82 | 83 | def _set_metadata(self, lock_file: IO, metadata: Metadata): 84 | lock_file.seek(0) 85 | serialized = pickle.dumps(metadata) 86 | lock_file.write(serialized) 87 | lock_file.truncate(len(serialized)) 88 | 89 | def is_empty(self, lock_file: IO) -> bool: 90 | return self._get_metadata(lock_file).is_empty 91 | 92 | def set_data(self, lock_file: IO, data: BytesLike): 93 | metadata = self._get_metadata(lock_file) 94 | assert metadata.is_empty, f'data of shared memory IPC payload {self._shm_name} has not yet been read' 95 | 96 | data_len = len(data) 97 | self._clear_shm() 98 | self._shm = SharedMemory(name=self._shm_name, create=True, size=data_len) 99 | self._shm.buf[:data_len] = data 100 | self._set_metadata(lock_file, self.Metadata(is_empty=False, size=data_len)) 101 | 102 | @contextlib.contextmanager 103 | def get_data(self, lock_file: IO) -> BytesLike: 104 | metadata = self._get_metadata(lock_file) 105 | assert not metadata.is_empty, f'metadata not found for shared memory IPC payload {self._shm_name}' 106 | 107 | shm = SharedMemory(name=self._shm_name) 108 | yield shm.buf[:metadata.size] 109 | shm.close() 110 | shm.unlink() 111 | self.clear(lock_file) 112 | 113 | def clear(self, lock_file: IO): 114 | self._set_metadata(lock_file, self.Metadata(is_empty=True, size=0)) 115 | 116 | def _clear_shm(self): 117 | try: 118 | if self._shm is None: 119 | self._shm = SharedMemory(name=self._shm_name) 120 | self._shm.close() 121 | self._shm.unlink() 122 | except FileNotFoundError: 123 | pass 124 | 125 | self._shm = None 126 | 127 | 128 | if platform.system().lower() == 'linux': 129 | OsFriendlyIpcStrategy = FileSystemIpcStrategy 130 | else: 131 | OsFriendlyIpcStrategy = SharedMemoryIpcStrategy 132 | -------------------------------------------------------------------------------- /lib_comfyui/platform_utils.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | 4 | def is_windows(): 5 | return 'windows' in platform.system().lower() 6 | -------------------------------------------------------------------------------- /lib_comfyui/torch_utils.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | 4 | 5 | def deep_to( 6 | tensor: Union[torch.Tensor, dict, list], 7 | *args, 8 | **kwargs, 9 | ) -> Union[torch.Tensor, dict, list]: 10 | if isinstance(tensor, torch.Tensor): 11 | tensor = tensor.to(*args, **kwargs) 12 | elif isinstance(tensor, dict): 13 | for k, v in tensor.items(): 14 | tensor[k] = deep_to(v, *args, **kwargs) 15 | elif isinstance(tensor, list): 16 | for i, v in enumerate(tensor): 17 | tensor[i] = deep_to(v, *args, **kwargs) 18 | elif isinstance(tensor, tuple): 19 | res = () 20 | for v in tensor: 21 | res += (deep_to(v, *args, **kwargs),) 22 | tensor = res 23 | 24 | return tensor 25 | -------------------------------------------------------------------------------- /lib_comfyui/webui/accordion.py: -------------------------------------------------------------------------------- 1 | import json 2 | import operator 3 | from typing import Tuple 4 | 5 | import gradio as gr 6 | from lib_comfyui import external_code, global_state 7 | from lib_comfyui.webui import gradio_utils, settings 8 | from lib_comfyui.comfyui import iframe_requests 9 | 10 | 11 | class AccordionInterface: 12 | def __init__(self, get_elem_id, tab): 13 | from modules import ui 14 | 15 | self.tab = tab 16 | 17 | self.workflow_types = external_code.get_workflow_types(self.tab) 18 | self.first_workflow_type = self.workflow_types[0] 19 | self.workflow_type_ids = { 20 | workflow_type.display_name: workflow_type.get_ids(self.tab)[0] 21 | for workflow_type in self.workflow_types 22 | } 23 | 24 | self.accordion = gr.Accordion( 25 | label='ComfyUI', 26 | open=False, 27 | elem_id=get_elem_id('accordion'), 28 | ) 29 | 30 | self.iframes = gr.HTML(value=self.get_iframes_html()) 31 | self.enabled_checkbox = gr.Checkbox( 32 | label='Enable', 33 | elem_id=get_elem_id('enable'), 34 | value=False, 35 | ) 36 | self.current_display_name = gr.Dropdown( 37 | label='Edit workflow type', 38 | choices=[workflow_type.display_name for workflow_type in self.workflow_types], 39 | value=self.first_workflow_type.display_name, 40 | elem_id=get_elem_id('current_display_name'), 41 | ) 42 | self.queue_front = gr.Checkbox( 43 | label='Queue front', 44 | elem_id=get_elem_id('queue_front'), 45 | value=True, 46 | ) 47 | self.refresh_button = gr.Button( 48 | value=f'{ui.refresh_symbol} Reload ComfyUI interfaces (client side)', 49 | elem_id=get_elem_id('refresh_button'), 50 | ) 51 | 52 | self.enabled_display_names = gradio_utils.ExtensionDynamicProperty( 53 | value=[], 54 | ) 55 | self.enabled_ids = gradio_utils.ExtensionDynamicProperty( 56 | value={ 57 | workflow_type_id: False 58 | for workflow_type_id in self.workflow_type_ids.values() 59 | }, 60 | ) 61 | self.clear_enabled_display_names_button = gr.Button( 62 | elem_id=get_elem_id('clear_enabled_display_names'), 63 | visible=False, 64 | ) 65 | 66 | self._rendered = False 67 | 68 | def get_iframes_html(self) -> str: 69 | comfyui_client_url = settings.get_comfyui_iframe_url() 70 | first_workflow_type_id = self.workflow_type_ids[self.first_workflow_type.display_name] 71 | 72 | iframes = [] 73 | for workflow_type_id in external_code.get_workflow_type_ids(self.tab): 74 | html_classes = [] 75 | if workflow_type_id == first_workflow_type_id: 76 | html_classes.append('comfyui-workflow-type-visible') 77 | 78 | iframes.append(f""" 79 | 85 | """) 86 | 87 | return f""" 88 |