diff --git a/action.yml b/action.yml index 91710b9..e587e89 100644 --- a/action.yml +++ b/action.yml @@ -5,21 +5,34 @@ inputs: description: "Operating System. macos, linux, windows." required: true default: "" + python_version: + description: "Python Version. Will be used in the conda install command for pytorch. 3.9 or 3.10. Only valid when os is linux or windows." + required: false + default: "3.9" cuda_version: description: "CUDA Version. Will be used in the conda install command for pytorch. 11.8 or 12.1. Only valid when os is linux or windows." required: false default: "12.1" + torch_version: + description: "Pytorch Version. Will be used in the conda install command for pytorch. 1.10.0 or 1.11.0. Only valid when os is linux or windows." + required: false + default: 'stable' models-json: description: 'JSON string containing models and their download URLs. The models will be downloaded into the exact directory relative to /ComfyUI/models/. eg { "model_name": { url: "https://example.com/model.pth", "directory": "checkpoints" } }' + required: false + workflow_filenames: + description: "List of workflows to run. Seperate by comma. eg. + 'workflow1,workflow2'. The list of workflow is listed in workflows/" required: true - workflow_name: - description: "Name of the workflow to run. This is used to identify the workflow in the logs." - required: true - workflow_json_path: - description: "Path (relative to the root of the Github repo) of the Workflow - JSON to run. Must be API format JSON." + comfyui_flags: + description: "Flags to pass to the comfyui application. eg. --force-fp16" required: false - default: "workflow.json" + default: '' + # Not yet supported + workflow_raw_json: + description: "Workflow's raw json file" + required: false + default: '' timeout: description: "Timeout for the workflow (in seconds)" required: false @@ -76,6 +89,7 @@ runs: miniconda-version: latest activate-environment: comfyui auto-activate-base: false + python-version: ${{ inputs.python_version }} - name: '[Unix-Mac-Only] Install Pytorch nightly' if: ${{ inputs.os == 'macos' }} @@ -127,13 +141,13 @@ runs: if: ${{ inputs.os != 'windows' }} shell: bash -el {0} run: | - python main.py --quick-test-for-ci + comfy launch -- --quick-test-for-ci - name: '[Unix] Run Python application' if: ${{ inputs.os != 'windows' }} shell: bash -el {0} run: | - python3 main.py --force-fp16 > application.log 2>&1 & + comfy launch -- --force-fp16 > application.log 2>&1 & - name: '[Unix] Check if the server is running' if: ${{ inputs.os != 'windows' }} @@ -155,43 +169,12 @@ runs: echo "Script output: " echo "$PROMPT_ID" - - name: '[Unix] Get start time' - id: unix_start_time - shell: bash - if: ${{ inputs.os != 'windows' }} - run: | - echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT - - - name: '[Unix] Check Prompt Status and Get Output Files' - if: ${{ inputs.os != 'windows' }} - shell: bash -el {0} - run: | - cd ${{ github.action_path }} - echo "Prompt ID: ${{ steps.unix_queue_prompt.outputs.prompt_id }}" - python3 check_prompt_status.py ${{ steps.unix_queue_prompt.outputs.prompt_id }} http://localhost:8188/history ${{ inputs.timeout }} - - - name: '[Unix] Get end time Unix' - id: unix_end_time - shell: bash - if: ${{ inputs.os != 'windows' }} - run: | - echo "end_time=$(date +%s)" >> $GITHUB_OUTPUT - - name: '[Unix] Auth to GCS' uses: "google-github-actions/auth@v2" if: ${{ inputs.os != 'windows' }} with: credentials_json: "${{ inputs.google_credentials }}" - - name: '[Unix] Upload Output Files to GCS' - if: ${{ success() && inputs.os != 'windows' }} - id: unix_upload-output-files - uses: google-github-actions/upload-cloud-storage@v2 - with: - path: ${{ github.workspace }}/output - destination: ${{ inputs.gcs_bucket_name }}/output-files/${{ github.job }}-${{ inputs.os }}-${{ inputs.workflow_name }}-run${{ github.run_id }} - glob: "${{ inputs.output_prefix }}*" - - name: '[Unix] Upload log file to GCS' if: ${{ ( success() || failure() ) && inputs.os != 'windows' }} id: unix_upload-log-files @@ -258,7 +241,7 @@ runs: start_time: $start_time, end_time: $end_time }') - + echo "$payload" response_code=$(curl -o "${{ github.workspace }}/application.log" \ @@ -266,12 +249,13 @@ runs: -X POST "${{inputs.api_endpoint}}" \ -H "Content-Type: application/json" \ -d "$payload") + if [[ $response_code -ne 200 ]]; then echo "API request failed with status code $response_code and response body" cat "${{ github.workspace }}/application.log" exit 1 fi - + - name: '[Unix] Upload Output Files' uses: actions/upload-artifact@v4 @@ -297,27 +281,27 @@ runs: run: conda deactivate && conda env remove --name comfyui && conda clean -all - ##################################################################################### - ## Windows Steps (F**k powershell) ## - ## ## - ## _.-;;-._ _ ## - ## '-..-'| || | | | ## - ## '-..-'|_.-;;-._| | |===( ) ////// ## - ## '-..-'| || | |_| ||| | o o| ## - ## '-..-'|_.-''-._| ||| ( c ) ____ ## - ## ||| \= / || \_ ## - ## |||||| || | ## - ## |||||| ...||__/|-" ## - ## |||||| __|________|__ ## - ## ||| |______________| ## - ## ||| || || || || ## - ## ||| || || || || ## - ## -------------------------------------|||-------------||-||------||-||------- ## - ## |__> || || || || ## - ## ## - ## ## - ##################################################################################### - + ##################################################################################### + ## Windows Steps (F**k powershell) ## + ## ## + ## _.-;;-._ _ ## + ## '-..-'| || | | | ## + ## '-..-'|_.-;;-._| | |===( ) ////// ## + ## '-..-'| || | |_| ||| | o o| ## + ## '-..-'|_.-''-._| ||| ( c ) ____ ## + ## ||| \= / || \_ ## + ## |||||| || | ## + ## |||||| ...||__/|-" ## + ## |||||| __|________|__ ## + ## ||| |______________| ## + ## ||| || || || || ## + ## ||| || || || || ## + ## -------------------------------------|||-------------||-||------||-||------- ## + ## |__> || || || || ## + ## ## + ## ## + ##################################################################################### + - name: '[Win] Setup Conda' uses: conda-incubator/setup-miniconda@v3.0.3 if: ${{ inputs.os == 'windows' }} @@ -325,6 +309,7 @@ runs: auto-update-conda: true miniconda-version: latest activate-environment: comfyui + python-version: ${{ inputs.python_version }} continue-on-error: true - name: '[Win-Only] Install Pytorch' diff --git a/check_prompt_status.py b/check_prompt_status.py index eb61dd0..e69de29 100644 --- a/check_prompt_status.py +++ b/check_prompt_status.py @@ -1,43 +0,0 @@ -import requests -import time -import sys - -def get_status(prompt_id, url): - response = requests.get(f"{url}/{prompt_id}") - if response.status_code == 200: - return response.json() - return None - -def is_completed(status_response, prompt_id): - # Check if the expected fields exist in the response - return ( - status_response and - prompt_id in status_response and - 'status' in status_response[prompt_id] and - status_response[prompt_id]['status'].get('completed', False) - ) - -def main(prompt_id, server_url, timeout): - start_time = time.time() - while True: - status_response = get_status(prompt_id, server_url) - if is_completed(status_response, prompt_id): - print("Prompt completed.") - break - - if time.time() - start_time > timeout: - print("Timeout reached without completion.") - sys.exit(1) - - time.sleep(10) # Check every 10 seconds - -if __name__ == "__main__": - if len(sys.argv) != 4: - print("Usage: python check_prompt_status.py ") - sys.exit(1) - - prompt_id_arg = sys.argv[1] - server_url_arg = sys.argv[2] - timeout_arg = int(sys.argv[3]) - - main(prompt_id_arg, server_url_arg, timeout_arg) diff --git a/queue_prompt.py b/queue_prompt.py index 90de3f8..3b2c79e 100644 --- a/queue_prompt.py +++ b/queue_prompt.py @@ -1,37 +1,127 @@ +import argparse import json +import subprocess +import datetime + import requests -import argparse + +from firebase_admin import storage def read_json_file(file_path): - with open(file_path, 'r') as file: + with open(file_path, 'r', encoding='utf-8') as file: return json.load(file) def post_json_to_server(json_data, url): return requests.post(url, json=json_data) -def main(json_file_path, server_url): - # Read JSON file - json_contents = read_json_file(json_file_path) - # Print the json_contents - #print(json.dumps(json_contents, indent=4)) - # Construct the new JSON object - data_to_send = {"prompt": json_contents} - - # Post the JSON to the server - response = post_json_to_server(data_to_send, server_url) - #print("Status Code:" + str(response.status_code)) - #print("Response:" + response.text) - response_json = response.json() - if ('prompt_id' not in response_json): - print("Error: prompt_id not found in response.") - print(response_json) +def get_status(prompt_id, url): + response = requests.get(f"{url}/{prompt_id}") + if response.status_code == 200: + return response.json() + return None + +def is_completed(status_response, prompt_id): + # Check if the expected fields exist in the response + return ( + status_response + and prompt_id in status_response + and 'status' in status_response[prompt_id] + and status_response[prompt_id]['status'].get('completed', False) + ) + +#TODO: add support for different file type +def upload_img_from_filename(bucket_name: str, gs_path: str, file_path: str, public: bool = True, content_type="image/png"): + bucket = storage.bucket(bucket_name) + blob = bucket.blob(gs_path) + blob.upload_from_filename(file_path, content_type=content_type) + if public: + blob.make_public() + + +def send_payload_to_api(args, output_files_gcs_paths, + start_time, end_time): + + # Create the payload as a dictionary + payload = { + "repo": args.repo, + "run_id": args.run_id, + "os": args.os, + "cuda_version": args.cuda_version, + "output_files_gcs_paths": output_files_gcs_paths, + "commit_hash": args.commit_hash, + "commit_time": args.commit_time, + "commit_message": args.commit_message, + "branch_name": args.branch_name, + "bucket_name": args.bucket_name, + "workflow_name": args.workflow_name, + "start_time": start_time, + "end_time": end_time + } + + # Convert payload dictionary to a JSON string + payload_json = json.dumps(payload) + + # Send POST request + headers = {'Content-Type': 'application/json'} + response = requests.post(args.api_endpoint, headers=headers, data=payload_json) + + # Write response to application.log + log_file_path = "./application.log" + with open(log_file_path, 'w', encoding='utf-8') as log_file: + log_file.write("\n##### Comfy CI Post Response #####\n") + log_file.write(response.text) + + # Check the response code + if response.status_code != 200: + print(f"API request failed with status code {response.status_code} and response body") + print(response.text) + exit(1) else: - print(response_json['prompt_id']) + print("API request successful") + + return response.status_code + +def main(args): + # Split the workflow file names using "," + workflow_files = args.comfy_workflow_names.split(',') + + counter = 1 + + for file_name in workflow_files: + # Construct the file path + file_path = f"workflows/{file_name}" + start_time = int(datetime.datetime.now().timestamp()) + subprocess.run( + ["comfycli", "run", "--workflow", file_path], + check=True, + ) + end_time = int(datetime.datetime.now().timestamp()) + + #TODO: add support for multiple file outputs + gs_path = f"output-files/{args.github_action_workflow_name}-{args.os}-{args.comfy_workflow_name}-run-${args.run_id}" + upload_img_from_filename(args.bucket_name, gs_path, f"{args.workspace_path}/output/{args.output_file_prefix}_{counter:05}_.png", public=True) + + send_payload_to_api(args, gs_path, start_time, end_time) + counter += 1 + if __name__ == "__main__": parser = argparse.ArgumentParser(description='Send a JSON file contents to a server as a prompt.') - parser.add_argument('json_file_path', type=str, help='Path to the JSON file to send.') - parser.add_argument('--server-url', type=str, default='http://localhost:8188/prompt', help='URL of the server to send the JSON to.') + parser.add_argument('--comfy-workflow-names', type=str, help='List of comfy workflow names.') + parser.add_argument('--github-action-workflow-name', type=str, help='Github action workflow name.') + parser.add_argument('--os', type=str, help='Operating system.') + parser.add_argument('--run-id', type=str, help='Github Run ID.') + parser.add_argument('--repo', type=str, help='Github repo.') + parser.add_argument('--cuda-version', type=str, help='CUDA version.') + parser.add_argument('--commit-hash', type=str, help='Commit hash.') + parser.add_argument('--commit-time', type=str, help='Commit time.') + parser.add_argument('--commit-message', type=str, help='Commit message.') + parser.add_argument('--branch-name', type=str, help='Branch name.') + parser.add_argument('--workflow-file-names', type=str, help='Workflow file names.') + parser.add_argument('--gsc-bucket-name', type=str, help='Name of the GCS bucket to store the output files in.') + parser.add_argument('--workspace-path', type=str, help='Workspace (ComfyUI repo) path, likely ${HOME}/action_runners/_work/ComfyUI/ComfyUI/.') + parser.add_argument('--action-path', type=str, help='Action path., likely ${HOME}/action_runners/_work/comfy-action/.') + parser.add_argument('--output-file-prefix', type=str, help='Output file prefix.') args = parser.parse_args() - main(args.json_file_path, args.server_url) + main(args) diff --git a/requirements.txt b/requirements.txt index f229360..9762e42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ requests +comfy-cli +firebase_admin \ No newline at end of file diff --git a/workflows/workflow_api.json b/workflows/default.json similarity index 100% rename from workflows/workflow_api.json rename to workflows/default.json diff --git a/workflows/workflow_lora_api.json b/workflows/lora.json similarity index 100% rename from workflows/workflow_lora_api.json rename to workflows/lora.json diff --git a/workflows/sd3_default.json b/workflows/sd3_default.json new file mode 100644 index 0000000..cbc362a --- /dev/null +++ b/workflows/sd3_default.json @@ -0,0 +1,186 @@ +{ + "6": { + "inputs": { + "text": "a female character with long, flowing hair that appears to be made of ethereal, swirling patterns resembling the Northern Lights or Aurora Borealis. The background is dominated by deep blues and purples, creating a mysterious and dramatic atmosphere. The character's face is serene, with pale skin and striking features. She wears a dark-colored outfit with subtle patterns. The overall style of the artwork is reminiscent of fantasy or supernatural genres", + "clip": [ + "11", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "11": { + "inputs": { + "clip_name1": "clip_g.safetensors", + "clip_name2": "clip_l.safetensors", + "clip_name3": "t5xxl_fp8_e4m3fn.safetensors" + }, + "class_type": "TripleCLIPLoader", + "_meta": { + "title": "TripleCLIPLoader" + } + }, + "13": { + "inputs": { + "shift": 3, + "model": [ + "252", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "ModelSamplingSD3" + } + }, + "67": { + "inputs": { + "conditioning": [ + "71", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" + } + }, + "68": { + "inputs": { + "start": 0.1, + "end": 1, + "conditioning": [ + "67", + 0 + ] + }, + "class_type": "ConditioningSetTimestepRange", + "_meta": { + "title": "ConditioningSetTimestepRange" + } + }, + "69": { + "inputs": { + "conditioning_1": [ + "68", + 0 + ], + "conditioning_2": [ + "70", + 0 + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" + } + }, + "70": { + "inputs": { + "start": 0, + "end": 0.1, + "conditioning": [ + "71", + 0 + ] + }, + "class_type": "ConditioningSetTimestepRange", + "_meta": { + "title": "ConditioningSetTimestepRange" + } + }, + "71": { + "inputs": { + "text": "bad quality, poor quality, doll, disfigured, jpg, toy, bad anatomy, missing limbs, missing fingers, 3d, cgi", + "clip": [ + "11", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Negative Prompt)" + } + }, + "135": { + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + }, + "class_type": "EmptySD3LatentImage", + "_meta": { + "title": "EmptySD3LatentImage" + } + }, + "231": { + "inputs": { + "samples": [ + "271", + 0 + ], + "vae": [ + "252", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "233": { + "inputs": { + "images": [ + "231", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "Preview Image" + } + }, + "252": { + "inputs": { + "ckpt_name": "sdv3/2b_1024/sd3_medium.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "271": { + "inputs": { + "seed": 945512652412924, + "steps": 28, + "cfg": 4.5, + "sampler_name": "dpmpp_2m", + "scheduler": "sgm_uniform", + "denoise": 1, + "model": [ + "13", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "69", + 0 + ], + "latent_image": [ + "135", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + } +} \ No newline at end of file diff --git a/workflows/sd3_multi_prompt.json b/workflows/sd3_multi_prompt.json new file mode 100644 index 0000000..8e85010 --- /dev/null +++ b/workflows/sd3_multi_prompt.json @@ -0,0 +1,189 @@ +{ + "11": { + "inputs": { + "clip_name1": "clip_g.safetensors", + "clip_name2": "clip_l.safetensors", + "clip_name3": "t5xxl_fp8_e4m3fn.safetensors" + }, + "class_type": "TripleCLIPLoader", + "_meta": { + "title": "TripleCLIPLoader" + } + }, + "13": { + "inputs": { + "shift": 3, + "model": [ + "252", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "ModelSamplingSD3" + } + }, + "67": { + "inputs": { + "conditioning": [ + "71", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" + } + }, + "68": { + "inputs": { + "start": 0.1, + "end": 1, + "conditioning": [ + "67", + 0 + ] + }, + "class_type": "ConditioningSetTimestepRange", + "_meta": { + "title": "ConditioningSetTimestepRange" + } + }, + "69": { + "inputs": { + "conditioning_1": [ + "68", + 0 + ], + "conditioning_2": [ + "70", + 0 + ] + }, + "class_type": "ConditioningCombine", + "_meta": { + "title": "Conditioning (Combine)" + } + }, + "70": { + "inputs": { + "start": 0, + "end": 0.1, + "conditioning": [ + "71", + 0 + ] + }, + "class_type": "ConditioningSetTimestepRange", + "_meta": { + "title": "ConditioningSetTimestepRange" + } + }, + "71": { + "inputs": { + "text": "bad quality, poor quality, doll, disfigured, jpg, toy, bad anatomy, missing limbs, missing fingers, 3d, cgi", + "clip": [ + "11", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Negative Prompt)" + } + }, + "135": { + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + }, + "class_type": "EmptySD3LatentImage", + "_meta": { + "title": "EmptySD3LatentImage" + } + }, + "231": { + "inputs": { + "samples": [ + "271", + 0 + ], + "vae": [ + "252", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "233": { + "inputs": { + "images": [ + "231", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "Preview Image" + } + }, + "252": { + "inputs": { + "ckpt_name": "sdv3/2b_1024/sd3_medium.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "271": { + "inputs": { + "seed": 945512652412924, + "steps": 28, + "cfg": 4.5, + "sampler_name": "dpmpp_2m", + "scheduler": "sgm_uniform", + "denoise": 1, + "model": [ + "13", + 0 + ], + "positive": [ + "273", + 0 + ], + "negative": [ + "69", + 0 + ], + "latent_image": [ + "135", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "273": { + "inputs": { + "clip_l": "the background is dominated by deep red and purples, creating a mysterious and dramatic atmosphere similar to a volcanic explosion", + "clip_g": "the background is dominated by deep red and purples, creating a mysterious and dramatic atmosphere similar to a volcanic explosion", + "t5xxl": "portrait of a female character with long, flowing hair that appears to be made of ethereal, swirling patterns resembling the Northern Lights or Aurora Borealis. Her face is serene, with pale skin and striking features. She wears a dark-colored outfit with subtle patterns. The overall style of the artwork is reminiscent of fantasy or supernatural genres\n", + "empty_padding": "none", + "clip": [ + "11", + 0 + ] + }, + "class_type": "CLIPTextEncodeSD3", + "_meta": { + "title": "CLIPTextEncodeSD3" + } + } +} \ No newline at end of file