commit 85203c0f4bb01999a573ed068f4428287782ec3b Author: kevin96666 Date: Sun Feb 18 19:00:31 2024 +0100 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8b301c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +**/__pycache__/ +**/*.pyc +**/*.pyo +**/*.pyd + +data/models/**/*.safetensors +data/models/**/*.pt +data/models/**/*.pth +data/models/**/*.ckpt +data/models/**/*.onnx +data/models/**/*.bin +data/models/**/*.yaml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2133938 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# SdfxForComfyUI - ComfyUI Custom Node for SDFX Integration + +## Overview + +SdfxForComfyUI is a custom node designed for seamless integration between ComfyUI and the SDFX solution. This custom node allows users to make ComfyUI compatible with SDFX when running the ComfyUI instance on their local machines. + +## Installation + +1. Clone the repository into the ComfyUI custom_node directory: + ```bash + git clone {repo_url} path/to/ComfyUI/custom_node/sdfx_node + ``` + +2. Navigate to the sdfx node directory: + ```bash + cd path/to/ComfyUI/custom_node/SdfxForComfyUI + ``` + +3. Install dependencies using pip: + ```bash + pip install -r requirements.txt + ``` + +## Configuration + +In the sdfx node directory, you will find a sample configuration file named `sdfx.config.example.json`. Rename this file to `sdfx.config.json` and customize the paths for various modules. Here is an example configuration: + +```json +{ + "args": { + "disable-xformers": false, + "preview-method": "taesd", + "listen": true, + "enable-cors-header": false, + "port": 8188 + }, + + "paths": { + "media": { + "gallery": "data/media/gallery", + "input": "data/media/input", + "output": "data/media/output", + "workflows": "data/media/workflows", + "templates": "data/media/templates", + "temp": "data/media/temp" + }, + + "models": { + "checkpoints": ["data/models/checkpoints"], + "clip": ["data/models/clip"], + // ... (other model paths) + } + } +} +``` +### Path Configuration + +- Each path under `paths` (e.g., `media`, `models`) represents a category of resources. +- Sub-keys (e.g., `gallery`, `checkpoints`) specify the specific resource paths. +- Paths can be relative, absolute, or omitted based on user preferences. +- If a path is relative, files will be read from the sdfx.config.json file's root. + + +## Usage + +Users can place if they want the `sdfx.config.json` file anywhere on their computer. To make it work, ComfyUI should be launched with the `--sdfx-config-file` flag: + +```bash +python main.py --sdfx-config-file=/path/to/sdfx.config.json +``` +Note: All relative paths configured in `sdfx.config.json` will be relative to the location of sdfx.config.json. +## License + +This project is licensed under the [MIT License](LICENSE). + +## Acknowledgments + +Special thanks to the SDFX and ComfyUI communities for their support and collaboration. + +For additional details and support, please refer to the [Contribution Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1b40fbc --- /dev/null +++ b/__init__.py @@ -0,0 +1,42 @@ +from functools import reduce +from .libs.prompt_parser import get_learned_conditioning_prompt_schedules,conditioning_set_timeset_range_prompt_mapper +from .sdfx_path import load_sdfx_extra_path_config + +load_sdfx_extra_path_config() + +from .server import add_sdfx_routes +add_sdfx_routes() + +class SDFXClipTextEncode: + @classmethod + def INPUT_TYPES(s): + return {"required": {"text": ("STRING", {"multiline": True}), "clip": ("CLIP", )}} + RETURN_TYPES = ("CONDITIONING",) + FUNCTION = "encode" + + CATEGORY = "conditioning" + + def encode(self, clip, text): + mapped_conditionings = list(map(conditioning_set_timeset_range_prompt_mapper(clip), (lambda p: get_learned_conditioning_prompt_schedules([text])[0])(text))) + return (reduce(lambda a,b: a+b, mapped_conditionings), ) + +from nodes import SaveImage + +def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE", ), + "filename_prefix": ("STRING", {"default": "SDFX"})}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + +SaveImage.INPUT_TYPES = classmethod(INPUT_TYPES) + +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "SDFXClipTextEncode": SDFXClipTextEncode +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "SDFXClipTextEncode": "SDFX Clip TextEncode (Prompt)" +} \ No newline at end of file diff --git a/data/media/gallery/gallery_goes_here b/data/media/gallery/gallery_goes_here new file mode 100644 index 0000000..e69de29 diff --git a/data/media/input/example-face-001.png b/data/media/input/example-face-001.png new file mode 100644 index 0000000..80ea8ef Binary files /dev/null and b/data/media/input/example-face-001.png differ diff --git a/data/media/input/example-face-002.png b/data/media/input/example-face-002.png new file mode 100644 index 0000000..d9b653d Binary files /dev/null and b/data/media/input/example-face-002.png differ diff --git a/data/media/input/example-face-003.png b/data/media/input/example-face-003.png new file mode 100644 index 0000000..19adde8 Binary files /dev/null and b/data/media/input/example-face-003.png differ diff --git a/data/media/input/example.png b/data/media/input/example.png new file mode 100644 index 0000000..f81cd50 Binary files /dev/null and b/data/media/input/example.png differ diff --git a/data/media/input/input_goes_here b/data/media/input/input_goes_here new file mode 100644 index 0000000..e69de29 diff --git a/data/media/output/output_goes_here b/data/media/output/output_goes_here new file mode 100644 index 0000000..e69de29 diff --git a/data/media/templates/template_goes_here b/data/media/templates/template_goes_here new file mode 100644 index 0000000..e69de29 diff --git a/data/media/workflows/workflow_goes_here b/data/media/workflows/workflow_goes_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/checkpoints/put_checkpoints_here b/data/models/checkpoints/put_checkpoints_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/clip/put_clip_or_text_encoder_models_here b/data/models/clip/put_clip_or_text_encoder_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/clip_vision/put_clip_vision_models_here b/data/models/clip_vision/put_clip_vision_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/configs/put_config_yamls_here b/data/models/configs/put_config_yamls_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/controlnet/put_controlnets_and_t2i_here b/data/models/controlnet/put_controlnets_and_t2i_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/diffusers/put_diffusers_models_here b/data/models/diffusers/put_diffusers_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/embeddings/put_embeddings_or_textual_inversion_concepts_here b/data/models/embeddings/put_embeddings_or_textual_inversion_concepts_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/facedetection/put_detection_Resnet50_and_parsing_parsenet_here b/data/models/facedetection/put_detection_Resnet50_and_parsing_parsenet_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/facerestore/put_GFPGAN_and_Codeformer_here b/data/models/facerestore/put_GFPGAN_and_Codeformer_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/gligen/put_gligen_models_here b/data/models/gligen/put_gligen_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/hypernetworks/put_hypernetworks_here b/data/models/hypernetworks/put_hypernetworks_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/insightface/put_insightface_models_here b/data/models/insightface/put_insightface_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/ipadapter/put_ipadapter_models_here b/data/models/ipadapter/put_ipadapter_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/loras/put_loras_here b/data/models/loras/put_loras_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/onnx/put_onnx_models_here b/data/models/onnx/put_onnx_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/sams/put_sam_models_here b/data/models/sams/put_sam_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/style_models/put_t2i_style_models_here b/data/models/style_models/put_t2i_style_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/ultralytics/put_ultralytics_here b/data/models/ultralytics/put_ultralytics_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/unet/put_unet_files_here b/data/models/unet/put_unet_files_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/upscale_models/put_esrgan_and_other_upscale_models_here b/data/models/upscale_models/put_esrgan_and_other_upscale_models_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/vae/put_vae_here b/data/models/vae/put_vae_here new file mode 100644 index 0000000..e69de29 diff --git a/data/models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here b/data/models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here new file mode 100644 index 0000000..e69de29 diff --git a/libs/__init__.py b/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/prompt_parser.py b/libs/prompt_parser.py new file mode 100644 index 0000000..7b14b7e --- /dev/null +++ b/libs/prompt_parser.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import re +from collections import namedtuple +from typing import List +import lark + +schedule_parser = lark.Lark(r""" +!start: (prompt | /[][():]/+)* +prompt: (emphasized | scheduled | plain | WHITESPACE)* +!emphasized: "(" prompt ")" + | "(" prompt ":" prompt ")" + | "[" prompt "]" + | "{" prompt "}" + | "{" (prompt "|" prompt)* "}" +scheduled: "[" [prompt ":"] prompt ":" [WHITESPACE] NUMBER "]" +WHITESPACE: /\s+/ +plain: /([^\\\[\]():|{}]|\\.)+/ +%import common.SIGNED_NUMBER -> NUMBER +""") + +def conditioning_set_timeset_range_prompt_mapper(clip): + previous_step = [0] + def wrapped(prompt): + start = previous_step[0] + end = prompt[0] + previous_step[0] = end + + tokens = clip.tokenize(prompt[1]) + cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) + conditioning = [[cond, {"pooled_output": pooled}]] + return set_range(conditioning, start, end) + + def set_range(conditioning, start, end): + c = [] + for t in conditioning: + d = t[1].copy() + d['start_percent'] = start + d['end_percent'] = end + n = [t[0], d] + c.append(n) + return c + return wrapped + +def get_learned_conditioning_prompt_schedules(prompts, env='prod'): + """ + >>> g = lambda p: get_learned_conditioning_prompt_schedules([p], 10)[0] + >>> g("test") + [[10, 'test']] + >>> g("a [b:3]") + [[3, 'a '], [10, 'a b']] + >>> g("a [b: 3]") + [[3, 'a '], [10, 'a b']] + >>> g("a [[[b]]:2]") + [[2, 'a '], [10, 'a [[b]]']] + >>> g("[(a:2):3]") + [[3, ''], [10, '(a:2)']] + >>> g("a [b : c : 1] d") + [[1, 'a b d'], [10, 'a c d']] + >>> g("a[b:[c:d:2]:1]e") + [[1, 'abe'], [2, 'ace'], [10, 'ade']] + >>> g("a [unbalanced") + [[10, 'a [unbalanced']] + >>> g("a [b:.5] c") + [[5, 'a c'], [10, 'a b c']] + >>> g("a [{b|d{:.5] c") # not handling this right now + [[5, 'a c'], [10, 'a {b|d{ c']] + >>> g("((a][:b:c [d:3]") + [[3, '((a][:b:c '], [10, '((a][:b:c d']] + >>> g("[a|(b:1.1)]") + [[1, 'a'], [2, '(b:1.1)'], [3, 'a'], [4, '(b:1.1)'], [5, 'a'], [6, '(b:1.1)'], [7, 'a'], [8, '(b:1.1)'], [9, 'a'], [10, '(b:1.1)']] + """ + + def collect_steps(tree): + res = [1] + + class CollectSteps(lark.Visitor): + def scheduled(self, tree): + tree.children[-1] = float(tree.children[-1]) + res.append(tree.children[-1]) + + CollectSteps().visit(tree) + return sorted(set(res)) + + def at_step(step, tree): + class AtStep(lark.Transformer): + def scheduled(self, args): + before, after, _, when = args + yield before or () if step <= when else after + def start(self, args): + def flatten(x): + if type(x) == str: + yield x + else: + for gen in x: + yield from flatten(gen) + return ''.join(flatten(args)) + def plain(self, args): + yield args[0].value + def __default__(self, data, children, meta): + for child in children: + yield child + return AtStep().transform(tree) + + def get_schedule(prompt): + try: + tree = schedule_parser.parse(prompt) + except lark.exceptions.LarkError as e: + if 0: + import traceback + traceback.print_exc() + if(env != 'prod'): + return {"error": "Illegal characters", "logs": str(e), "prompt": prompt} + else: + return [[1, prompt]] + return [[t, at_step(t, tree)] for t in collect_steps(tree)] + + promptdict = {prompt: get_schedule(prompt) for prompt in set(prompts)} + return [promptdict[prompt] for prompt in prompts] + + +if __name__ == "__main__": + import doctest + doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE) +else: + import torch # doctest faster \ No newline at end of file diff --git a/prestartup_script.py b/prestartup_script.py new file mode 100644 index 0000000..ba8128c --- /dev/null +++ b/prestartup_script.py @@ -0,0 +1,14 @@ +import argparse + +# Hack to pass extra args to main.py +def parse_args_override(self, args=None, namespace=None): + namespace, _ = self.parse_known_args(args, namespace) + return namespace + +argparse.ArgumentParser.parse_args = parse_args_override + +parser = argparse.ArgumentParser() +parser.add_argument("--sdfx-config-file", type=str, default=None, help="Path for sdfx config file") +args = parser.parse_args() + +#args.sdfx_config_file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ade3771 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lark \ No newline at end of file diff --git a/sdfx.config.example.json b/sdfx.config.example.json new file mode 100644 index 0000000..bbb7117 --- /dev/null +++ b/sdfx.config.example.json @@ -0,0 +1,87 @@ +{ + "args": { + "disable-xformers": false, + "preview-method": "taesd", + "listen": true, + "enable-cors-header": false, + "port": 8188 + }, + + "paths": { + + "media": { + "gallery": "data/media/gallery", + "input": "data/media/input", + "output": "data/media/output", + "workflows": "data/media/workflows", + "templates": "data/media/templates", + "temp": "data/media/temp" + }, + + "models": { + "checkpoints": [ + "data/models/checkpoints" + ], + "clip": [ + "data/models/clip" + ], + "clip_vision": [ + "data/models/clip_vision" + ], + "configs": [ + "data/models/configs" + ], + "controlnet": [ + "data/models/controlnet" + ], + "diffusers": [ + "data/models/diffusers" + ], + "embeddings": [ + "data/models/embeddings" + ], + "facedetection": [ + "data/models/facedetection" + ], + "face_restore": [ + "data/models/facerestore" + ], + "gligen": [ + "data/models/gligen" + ], + "insightface": [ + "data/models/insightface" + ], + "ipadapter": [ + "data/models/ipadapter" + ], + "hypernetworks": [ + "data/models/hypernetworks" + ], + "loras": [ + "data/models/loras" + ], + "sams": [ + "data/models/sams" + ], + "ultralytics": [ + "data/models/ultralytics" + ], + "style_models": [ + "data/models/style_models" + ], + "unet": [ + "data/models/unet" + ], + "upscale_models": [ + "data/models/upscale_models" + ], + "vae": [ + "data/models/vae" + ], + "vae_approx": [ + "data/models/vae_approx" + ] + } + } +} \ No newline at end of file diff --git a/sdfx_path.py b/sdfx_path.py new file mode 100644 index 0000000..d9e6513 --- /dev/null +++ b/sdfx_path.py @@ -0,0 +1,110 @@ +import os +import json +import folder_paths +from .prestartup_script import args + +sdfx_config_file_name = 'sdfx.config.json' + +#=== Best effort to find sdfx_config_file_path === +def find_sdfx_config_path(): + + if args.sdfx_config_file is not None: + return args.sdfx_config_file + + path = os.path.abspath(os.path.dirname(__file__)) + found_path = None + try: + for _ in range(6): + config_path = os.path.join(path, sdfx_config_file_name) + if os.path.exists(config_path): + found_path = path + + path = os.path.dirname(path) + except Exception as e: + print(f"Error File access : {e}") + + return found_path + +#=== if path is relative return absolute === +def get_sdfx_absolute_path(path): + if os.path.isabs(path): + return path + else: + return os.path.join(find_sdfx_config_path(), path) + +gallery_path = get_sdfx_absolute_path('data/media/gallery') +workflows_path = get_sdfx_absolute_path('data/media/worflows') +templates_path = get_sdfx_absolute_path('data/media/gallery') + +#=== add all sdfx path to comfy folder_paths === +def load_sdfx_extra_path_config(): + global gallery_path, workflows_path, templates_path + + sdfx_config_path = find_sdfx_config_path() + if(sdfx_config_path is None): + return + + sdfx_config_file = os.path.join(sdfx_config_path, sdfx_config_file_name) + try: + with open(sdfx_config_file, 'r') as json_file: + data = json.load(json_file) + + #=== load sdfx models === + models = data.get('paths', {}).get('models', None) + if isinstance(models, dict): + for m in models: + for x in models[m]: + full_path = get_sdfx_absolute_path(x) + print("Adding sdfx extra search path", m, full_path) + folder_paths.add_model_folder_path(m, full_path) + + #=== load sdfx medias === + media = data.get('paths', {}).get('media', {}) + o = media.get('output', None) + if isinstance(o, str): + output_dir = get_sdfx_absolute_path(o) + print("Setting sdfx output path", output_dir) + folder_paths.set_output_directory(output_dir) + + i = media.get('input', None) + if isinstance(i, str): + input_dir = get_sdfx_absolute_path(i) + print("Setting sdfx input path", input_dir) + folder_paths.set_input_directory(input_dir) + + t = media.get('temp', None) + if isinstance(t, str): + temp_dir = get_sdfx_absolute_path(t) + print("Setting sdfx temp path", temp_dir) + folder_paths.set_temp_directory(temp_dir) + + g = media.get('gallery', None) + if isinstance(g, str): + gallery_path = get_sdfx_absolute_path(g) + print("Setting sdfx gallery path", gallery_path) + + w = media.get('workflows', None) + if isinstance(w, str): + workflows_path = get_sdfx_absolute_path(w) + print("Setting sdfx workflows path", workflows_path) + + te = media.get('templates', None) + if isinstance(te, str): + templates_path = get_sdfx_absolute_path(te) + print("Setting sdfx templates path", templates_path) + + except json.JSONDecodeError as e: + print(f"[FATAL] {sdfx_config_file_name} is not valid, it will not be loaded !") + except FileNotFoundError: + print(f"[FATAL] {sdfx_config_file_name} not found") + except Exception as e: + print(f"[FATAL] : {e}") + +def get_gallery_path(): + return gallery_path + +def get_workflows_path(): + return workflows_path + +def get_templates_path(): + return templates_path \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..5d804b4 --- /dev/null +++ b/server.py @@ -0,0 +1,330 @@ +import os +from aiohttp import web +import server +import hashlib +import folder_paths +import uuid +import time +import json +from .libs.prompt_parser import get_learned_conditioning_prompt_schedules +from .sdfx_path import get_gallery_path, get_workflows_path, get_templates_path + +gallery_path = get_gallery_path() +workflows_path = get_workflows_path() +templates_path = get_templates_path() + +def get_media_url_from_params(url_path, filename, type, gallery): + return url_path + '/media?filename=' + filename + '&type=' + type + '&gallery=' + gallery + +def get_dir_by_model_id_and_type(model_id, dir_type): + # === Todo handle other media types === + if dir_type is None or dir_type not in ["image"]: + dir_type = "image" + + if dir_type == "image": + type_dir = os.path.join(gallery_path, model_id, "image") + + return type_dir, dir_type + +def media_upload(post, url_path): + image = post.get("file") + overwrite = post.get("overwrite") + model_id = post.get("modelId") + + image_upload_type = post.get("type") + upload_dir, image_upload_type = get_dir_by_model_id_and_type(model_id, image_upload_type) + + if image and image.file: + filename = image.filename + if not filename: + return web.Response(status=400) + + filepath = os.path.abspath(os.path.join(upload_dir, filename)) + + if os.path.commonpath((upload_dir, filepath)) != upload_dir: + return web.Response(status=400) + + if not os.path.exists(upload_dir): + os.makedirs(upload_dir) + + split = os.path.splitext(filename) + + if overwrite is not None and (overwrite == "true" or overwrite == "1"): + pass + else: + i = 1 + while os.path.exists(filepath): + filename = f"{split[0]} ({i}){split[1]}" + filepath = os.path.join(upload_dir, filename) + i += 1 + + with open(filepath, "wb") as f: + f.write(image.file.read()) + return web.json_response({"name" : filename, "type": image_upload_type, "path": get_media_url_from_params(url_path, filename, image_upload_type, model_id)}) + else: + return web.Response(status=400) + +def get_domain_from_request(request): + return request.scheme + '://' + request.host + +def get_medias_from_model_id(media_id, url_path): + media_type = 'image' + # === Todo handle other media types === + media_folder = os.path.join(gallery_path, media_id, media_type) + if os.path.exists(media_folder) and os.path.isdir(media_folder): + return [{"name": image, "type": media_type, "path": get_media_url_from_params(url_path, image, media_type, media_id)} for image in os.listdir(media_folder)] + + return [] + +def add_sdfx_routes(): + # === Comfy determines the path of a checkpoint by its name === + # === If two different paths contain two checkpoints with the same name, there is an arbitration done to choose the appropriate path === + @server.PromptServer.instance.routes.get("/sdfx/model/list") + def get_models(request): + url_path = get_domain_from_request(request) + models = [] + cns = folder_paths.get_filename_list('checkpoints') + print(cns) + for cn in cns: + fp = folder_paths.get_full_path("checkpoints", cn) + media_id = hashlib.md5(fp.encode()).hexdigest() + models.append({ + "modelId": media_id, + "name": cn, + "type": "checkpoint", + "gallery": get_medias_from_model_id(media_id, url_path) + }) + + return web.json_response(models) + + # === Single API to upload all kind of assets === + @server.PromptServer.instance.routes.post("/sdfx/media") + async def upload_media(request): + post = await request.post() + url_path = get_domain_from_request(request) + return media_upload(post, url_path) + + + @server.PromptServer.instance.routes.get("/sdfx/media") + async def view_media(request): + if "filename" not in request.rel_url.query: + return web.Response(status=400) + if "type" not in request.rel_url.query: + return web.Response(status=400) + if "gallery" not in request.rel_url.query: + return web.Response(status=400) + + filename = request.rel_url.query["filename"] + type = request.rel_url.query["type"] + gallery = request.rel_url.query["gallery"] + + if filename[0] == '/' or '..' in filename: + return web.Response(status=400) + if type[0] == '/' or '..' in type: + return web.Response(status=400) + if gallery[0] == '/' or '..' in gallery: + return web.Response(status=400) + + file = os.path.join(gallery_path, gallery, type, filename) + + if os.path.isfile(file): + if 'preview' in request.rel_url.query: + with Image.open(file) as img: + preview_info = request.rel_url.query['preview'].split(';') + image_format = preview_info[0] + if image_format not in ['webp', 'jpeg'] or 'a' in request.rel_url.query.get('channel', ''): + image_format = 'webp' + + quality = 90 + if preview_info[-1].isdigit(): + quality = int(preview_info[-1]) + + buffer = BytesIO() + if image_format in ['jpeg'] or request.rel_url.query.get('channel', '') == 'rgb': + img = img.convert("RGB") + img.save(buffer, format=image_format, quality=quality) + buffer.seek(0) + + return web.Response(body=buffer.read(), content_type=f'image/{image_format}', + headers={"Content-Disposition": f"filename=\"{filename}\""}) + + if 'channel' not in request.rel_url.query: + channel = 'rgba' + else: + channel = request.rel_url.query["channel"] + + if channel == 'rgb': + with Image.open(file) as img: + if img.mode == "RGBA": + r, g, b, a = img.split() + new_img = Image.merge('RGB', (r, g, b)) + else: + new_img = img.convert("RGB") + + buffer = BytesIO() + new_img.save(buffer, format='PNG') + buffer.seek(0) + + return web.Response(body=buffer.read(), content_type='image/png', headers={"Content-Disposition": f"filename=\"{filename}\""}) + + elif channel == 'a': + with Image.open(file) as img: + if img.mode == "RGBA": + _, _, _, a = img.split() + else: + a = Image.new('L', img.size, 255) + + # alpha img + alpha_img = Image.new('RGBA', img.size) + alpha_img.putalpha(a) + alpha_buffer = BytesIO() + alpha_img.save(alpha_buffer, format='PNG') + alpha_buffer.seek(0) + + return web.Response(body=alpha_buffer.read(), content_type='image/png', headers={"Content-Disposition": f"filename=\"{filename}\""}) + else: + return web.FileResponse(file, headers={"Content-Disposition": f"filename=\"{filename}\""}) + + return web.Response(status=400) + + @server.PromptServer.instance.routes.get("/sdfx/prompt_parser") + async def prompt_parser(request): + if "prompt" in request.rel_url.query: + prompt = request.rel_url.query["prompt"] + env = request.rel_url.query['env'] if 'env' in request.rel_url.query else 'prod' + g = lambda p: get_learned_conditioning_prompt_schedules([p], env)[0] + return web.json_response({'prompt' : prompt, 'data' : g(prompt)}) + return web.Response(status=404) + + @server.PromptServer.instance.routes.get("/sdfx/workflow/list") + @server.PromptServer.instance.routes.get("/sdfx/template/list") + async def workflows_templates(request): + list = [] + path = request.url.path + + if path == "/sdfx/workflow/list": + working_dir = workflows_path + elif path == "/sdfx/template/list": + working_dir = templates_path + + for filename in os.listdir(working_dir): + if filename.endswith(".json"): + file_path = os.path.join(working_dir, filename) + with open(file_path, 'r') as file: + try: + data = json.load(file) + uid = data.get('uid') + name = data.get('name') + dateCreated = data.get('dateCreated') + metas = data.get('metas') + list.append({ 'uid': uid, 'name': name, 'metas': metas, 'dateCreated': dateCreated}) + + except json.JSONDecodeError as e: + return web.json_response({'error': 'invalid Json'}, status=400) + return web.json_response(list) + + @server.PromptServer.instance.routes.get("/sdfx/workflow") + @server.PromptServer.instance.routes.post("/sdfx/workflow") + @server.PromptServer.instance.routes.put("/sdfx/workflow") + @server.PromptServer.instance.routes.delete("/sdfx/workflow") + @server.PromptServer.instance.routes.get("/sdfx/template") + @server.PromptServer.instance.routes.post("/sdfx/template") + @server.PromptServer.instance.routes.put("/sdfx/template") + @server.PromptServer.instance.routes.delete("/sdfx/template") + async def workflow_template(request): + method = request.method + path = request.url.path + if method == 'GET': + uid = request.query.get('uid') + if not uid: + return web.json_response({'error': 'uid missing'}, status=400) + if path == "/sdfx/workflow": + filename = os.path.join(workflows_path, f"{uid}.json") + elif path == "/sdfx/template": + filename = os.path.join(templates_path, f"{uid}.json") + if not os.path.isfile(filename): + return web.json_response({'error': 'file missing'}, status=404) + with open(filename, 'r') as file: + file_content = json.load(file) + return web.json_response(file_content) + elif method == 'POST' or method == 'PUT': + try: + data = await request.json() + data['dateCreated'] = int(time.time()) + if(method == 'POST'): + hash = str(uuid.uuid4()) + else: + hash = request.query.get('uid') + if not hash: + return web.json_response({'error': 'uid missing'}, status=400) + data['uid'] = hash + if path == "/sdfx/workflow": + filename = os.path.join(workflows_path, f"{hash}.json") + elif path == "/sdfx/template": + filename = os.path.join(templates_path, f"{hash}.json") + with open(filename, 'w') as file: + json.dump(data, file) + response = {'uid': data.get('uid'), 'name': data.get('name'), 'metas': data.get('meta'), 'dateCreated': data.get('dateCreated')} + return web.json_response(response) + except json.JSONDecodeError: + return web.json_response({'error': 'invalid Json'}, status=400) + elif method == 'DELETE': + uid = request.query.get('uid') + if not uid: + return web.json_response({'error': "Fatal: Parameter 'uid' missing"}, status=400) + if path == "/sdfx/workflow": + filename = os.path.join(workflows_path, f"{uid}.json") + elif path == "/sdfx/template": + filename = os.path.join(templates_path, f"{uid}.json") + if os.path.isfile(filename): + os.remove(filename) + return web.json_response({'uid': uid, 'status':'deleted'}) + else: + return web.Response(status=404) + + + @server.PromptServer.instance.routes.get("/sdfx/input") + @server.PromptServer.instance.routes.delete("/sdfx/input") + @server.PromptServer.instance.routes.get("/sdfx/output") + @server.PromptServer.instance.routes.delete("/sdfx/output") + async def input_output(request): + method = request.method + path = request.url.path + if method == 'GET': + name = request.query.get('name') + if not name: + return web.json_response({'error': 'name missing'}, status=400) + if path == '/sdfx/input': + filename = os.path.join(folder_paths.get_input_directory(), name) + elif path == '/sdfx/output': + filename = os.path.join(folder_paths.get_output_directory(), name) + if not os.path.isfile(filename): + return web.json_response({'error': 'file missing'}, status=404) + return web.json_response({'name': name}) + if method == 'DELETE': + name = request.query.get('name') + if not name: + return web.json_response({'error': 'name missing'}, status=400) + if path == '/sdfx/input': + filename = os.path.join(folder_paths.get_input_directory(), name) + elif path == '/sdfx/output': + filename = os.path.join(folder_paths.get_output_directory(), name) + if os.path.isfile(filename): + os.remove(filename) + return web.json_response({'name': name, 'status':'deleted'}) + + @server.PromptServer.instance.routes.get("/sdfx/input/list") + @server.PromptServer.instance.routes.get("/sdfx/output/list") + async def inputs_outputs(request): + list = [] + path = request.url.path + + if path == '/sdfx/input/list': + working_dir = folder_paths.get_input_directory() + elif path == '/sdfx/output/list': + working_dir = folder_paths.get_output_directory() + + for filename in os.listdir(working_dir): + if '.' in filename: + list.append({'name': filename}) + return web.json_response(list) \ No newline at end of file