├── .github └── workflows │ └── update_space.yml ├── .gitignore ├── README.md ├── assets └── cover.png ├── main.py └── requirements.txt /.github/workflows/update_space.yml: -------------------------------------------------------------------------------- 1 | name: Run Python script 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.9' 20 | 21 | - name: Install Gradio 22 | run: python -m pip install gradio 23 | 24 | - name: Log in to Hugging Face 25 | run: python -c 'import huggingface_hub; huggingface_hub.login(token="${{ secrets.hf_token }}")' 26 | 27 | - name: Deploy to Spaces 28 | run: gradio deploy 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | venv 7 | flagged 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: comfyui-deploy-gradio 3 | app_file: main.py 4 | sdk: gradio 5 | sdk_version: 4.41.0 6 | --- 7 | # ComfyDeploy Gradio Interface 8 | 9 | This project provides a Gradio interface for interacting with ComfyDeploy, allowing users to dynamically generate UI components based on deployment input definitions and submit jobs to ComfyDeploy. 10 | 11 | ![ComfyDeploy Gradio Interface](assets/cover.png) 12 | 13 | ## Features 14 | 15 | - Dynamic UI generation based on ComfyDeploy input definitions 16 | - Support for various input types (text, image, number, checkbox, etc.) 17 | - Asynchronous job submission to ComfyDeploy 18 | - Real-time progress tracking 19 | - Display of output images and text 20 | 21 | ## Installation 22 | 23 | 1. Clone this repository 24 | 2. Install the required dependencies: 25 | 26 | ```bash 27 | pip install comfydeploy gradio pillow requests python-dotenv 28 | ``` 29 | 30 | 3. Create a `.env` file in the project root with the following content: 31 | 32 | ``` 33 | API_KEY=your_comfydeploy_api_key 34 | DEPLOYMENT_ID=your_deployment_id 35 | ``` 36 | 37 | ## Usage 38 | 39 | Run the main script: 40 | 41 | ```bash 42 | python main.py 43 | ``` 44 | 45 | This will launch a Gradio interface in your default web browser. The interface will dynamically generate input components based on your ComfyDeploy deployment configuration. You can then input values and submit jobs to ComfyDeploy. 46 | 47 | ## How it works 48 | 49 | 1. The script fetches input definitions from ComfyDeploy. 50 | 2. It dynamically generates Gradio components based on these definitions. 51 | 3. When the user submits inputs, it sends a job to ComfyDeploy. 52 | 4. The script then polls for job completion and displays the results (images and text). 53 | 54 | ## Contributing 55 | 56 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 57 | 58 | ## License 59 | 60 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comfy-deploy/comfyui-deploy-gradio-demo/47da384eb4ec826d94ee86a05cd7979eba5a9b11/assets/cover.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from comfydeploy import ComfyDeploy 2 | import asyncio 3 | import os 4 | import gradio as gr 5 | from PIL import Image 6 | import requests 7 | import dotenv 8 | from io import BytesIO 9 | import random 10 | # from gradio_imageslider import ImageSlider 11 | 12 | dotenv.load_dotenv() 13 | 14 | client = ComfyDeploy(bearer_auth=os.environ['API_KEY']) 15 | deployment_id = os.environ['DEPLOYMENT_ID'] 16 | 17 | def get_gradio_component(class_type): 18 | component_map = { 19 | 'ComfyUIDeployExternalText': gr.Textbox, 20 | 'ComfyUIDeployExternalImage': gr.Image, 21 | 'ComfyUIDeployExternalImageAlpha': gr.Image, 22 | 'ComfyUIDeployExternalNumber': gr.Number, 23 | 'ComfyUIDeployExternalNumberInt': gr.Number, 24 | 'ComfyUIDeployExternalLora': gr.Textbox, 25 | 'ComfyUIDeployExternalCheckpoint': gr.Textbox, 26 | 'ComfyDeployWebscoketImageInput': gr.Image, 27 | 'ComfyUIDeployExternalImageBatch': gr.File, 28 | 'ComfyUIDeployExternalVideo': gr.Video, 29 | 'ComfyUIDeployExternalBoolean': gr.Checkbox, 30 | 'ComfyUIDeployExternalNumberSlider': gr.Slider, 31 | } 32 | return component_map.get(class_type, gr.Textbox) # Default to Textbox if not found 33 | 34 | with gr.Blocks() as demo: 35 | gr.Markdown(""" 36 | # ComfyDeploy Gradio Interface 37 | 38 | This is a Gradio interface for a ComfyDeploy workflow. You can interact with the deployed model using the inputs below. 39 | 40 | To clone this workflow, visit: [ComfyDeploy Gradio Flux](https://www.comfydeploy.com/share/comfy-deploy-gradio-flux) 41 | 42 | ## Example usage of ComfyDeploy SDK: 43 | 44 | ```python 45 | from comfydeploy import ComfyDeploy 46 | import os 47 | 48 | # Initialize the client 49 | client = ComfyDeploy(bearer_auth=os.environ['API_KEY']) 50 | 51 | # Run the model 52 | inputs = { 53 | 'prompt': 'A beautiful landscape', 54 | 'negative_prompt': 'ugly, blurry', 55 | 'width': 512, 56 | 'height': 512 57 | } 58 | res = client.run.create( 59 | request={ 60 | "deployment_id": deployment_id, 61 | "inputs": inputs 62 | } 63 | ) 64 | 65 | # Get the results 66 | run_id = res.object.run_id 67 | result = client.run.get(run_id=run_id) 68 | ``` 69 | """) 70 | 71 | def randomSeed(num_digits=15): 72 | range_start = 10 ** (num_digits - 1) 73 | range_end = (10**num_digits) - 1 74 | return random.randint(range_start, range_end) 75 | 76 | # Function to update inputs 77 | def get_inputs(): 78 | res = client.deployment.get_input_definition(id=deployment_id) 79 | input_definitions = res.response_bodies 80 | gradio_inputs = [] 81 | random_seeds = [] 82 | for input_def in input_definitions: 83 | component_class = get_gradio_component(input_def.class_type) 84 | 85 | kwargs = { 86 | "label": input_def.input_id, 87 | "value": input_def.default_value 88 | } 89 | 90 | print(kwargs) 91 | 92 | if input_def.class_type == 'ComfyUIDeployExternalNumberSlider': 93 | kwargs.update({ 94 | "minimum": input_def.min_value, 95 | "maximum": input_def.max_value 96 | }) 97 | elif input_def.class_type in ['ComfyUIDeployExternalImage', 'ComfyUIDeployExternalImageAlpha', 'ComfyDeployWebscoketImageInput']: 98 | kwargs["type"] = "filepath" 99 | elif input_def.class_type == 'ComfyUIDeployExternalImageBatch': 100 | kwargs["file_count"] = "multiple" 101 | elif input_def.class_type == 'ComfyUIDeployExternalNumberInt': 102 | kwargs["precision"] = 0 103 | 104 | if "seed" in input_def.input_id: 105 | with gr.Row(): 106 | kwargs["value"] = randomSeed() 107 | input = component_class(**kwargs, scale=6) 108 | randomize_button = gr.Button("Randomize", size="sm") 109 | def randomize_seed(input): 110 | return randomSeed() 111 | randomize_button.click(fn=randomize_seed, inputs=input, outputs=input) 112 | gradio_inputs.append(input) 113 | random_seeds.append(input) 114 | 115 | # print(kwargs) 116 | else: 117 | gradio_inputs.append(component_class(**kwargs)) 118 | 119 | return gradio_inputs, input_definitions, random_seeds 120 | 121 | with gr.Blocks() as demo: 122 | gr.Markdown(""" 123 | # ComfyDeploy Gradio Interface 124 | 125 | This is a demo Gradio interface for a ComfyUI workflow deployed on ComfyDeploy as backend and Gradio as frontend. 126 | 127 | GitHub: [Source Code](https://github.com/comfy-deploy/comfyui-deploy-gradio-demo) 128 | 129 | To clone this ComfyUI workflow and deploy, visit: [ComfyDeploy Flux Workflow Demo](https://www.comfydeploy.com/share/comfy-deploy-gradio-flux) 130 | 131 | Model Using 132 | - flux schnell 133 | - [Workflow Modified from markury](https://civitai.com/models/618997/simpleadvanced-flux1-comfyui-workflows) 134 | - [Optional lora form ogkai, nux](https://civitai.com/models/636355/flux-detailer) 135 | 136 | ComfyDeploy deploy any ComfyUI workflow, install any custom nodes and models. *subject to individual custom nodes and models licenses* 137 | """) 138 | 139 | with gr.Row(): 140 | with gr.Column(scale=1): 141 | @gr.render() 142 | def update_inputs(): 143 | inputs, input_definitions, random_seeds = get_inputs() 144 | submit_button = gr.Button("Submit") 145 | 146 | async def main(*args, progress=gr.Progress()): 147 | inputs = {input_def.input_id: arg for input_def, arg in zip(input_definitions, args)} 148 | 149 | for key, value in inputs.items(): 150 | if isinstance(value, list) and all(isinstance(url, str) for url in value): 151 | inputs[key] = [requests.get(url).content for url in value] 152 | elif isinstance(value, str) and value.startswith('http'): 153 | inputs[key] = requests.get(value).content 154 | 155 | res = await client.run.create_async( 156 | request={ 157 | "deployment_id": deployment_id, 158 | "inputs": inputs 159 | }) 160 | 161 | images = [] 162 | text = "" 163 | outputs = [ 164 | images, 165 | text 166 | ] 167 | while True: 168 | if res.object is not None: 169 | res2 = await client.run.get_async(run_id=res.object.run_id) 170 | print("checking ", res2.object.progress, res2.object.live_status) 171 | progress_value = res2.object.progress if res2.object.progress is not None else 0 172 | progress(progress_value, desc=f"{res2.object.live_status if res2.object.live_status is not None else 'Cold starting...'}") 173 | 174 | if res2.object is not None and res2.object.status == "success": 175 | # print(res2) 176 | for output in res2.object.outputs: 177 | print(output.data.images) 178 | if output.data.images: 179 | urls = [image.url for image in output.data.images] 180 | for url in urls: 181 | response = requests.get(url) 182 | img = Image.open(BytesIO(response.content)) 183 | outputs[0].append(img) 184 | elif output.data.text: 185 | print(output.data.text) 186 | outputs[1] += "\n\n" + "\n".join(output.data.text) 187 | break 188 | await asyncio.sleep(2) 189 | 190 | random_seed_output = [] 191 | for random_seed in random_seeds: 192 | random_seed_output.append(randomSeed()) 193 | 194 | return outputs + random_seed_output 195 | 196 | submit_button.click(fn=main, inputs=inputs, outputs=output_components+random_seeds) 197 | 198 | with gr.Column(scale=1): 199 | output_components = [ 200 | gr.Gallery(), 201 | gr.Textbox(label="Text Output"), 202 | ] 203 | 204 | 205 | gr.Markdown(""" 206 | ## Example usage of ComfyDeploy SDK: 207 | 208 | ```python 209 | from comfydeploy import ComfyDeploy 210 | import os 211 | 212 | # Initialize the client 213 | client = ComfyDeploy(bearer_auth=os.environ['API_KEY']) 214 | 215 | # Run the model 216 | inputs = { 217 | 'prompt': 'A beautiful landscape', 218 | 'negative_prompt': 'ugly, blurry', 219 | 'width': 512, 220 | 'height': 512 221 | } 222 | res = client.run.create( 223 | request={ 224 | "deployment_id": deployment_id, 225 | "inputs": inputs 226 | } 227 | ) 228 | 229 | # Get the results 230 | run_id = res.object.run_id 231 | result = client.run.get(run_id=run_id) 232 | """) 233 | 234 | if __name__ == "__main__": 235 | demo.launch(share=True) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | comfydeploy==0.3.4 2 | gradio==4.41.0 3 | gradio_imageslider 4 | python-dotenv --------------------------------------------------------------------------------