├── .github └── workflows │ └── publish.yml ├── .gitignore ├── README.md ├── __init__.py ├── data ├── sdxl_styles_base.json ├── sdxl_styles_sai.json └── sdxl_styles_twri.json ├── eagleapi ├── api_application.py ├── api_folder.py ├── api_item.py └── api_util.py ├── prompt_parser.py ├── pyproject.toml ├── sdxl_prompt_styler.py └── tests ├── __init__.py ├── examples ├── 01.json ├── 02.json └── 03-SDXL.json └── test_prompt_parser.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - "pyproject.toml" 10 | 11 | jobs: 12 | publish-node: 13 | name: Publish Custom Node to registry 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v4 18 | - name: Publish Custom Node 19 | uses: Comfy-Org/publish-node-action@main 20 | with: 21 | ## Add your own personal access token to your Github Repository secrets and reference it here. 22 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | .idea 104 | config.yaml 105 | .volume 106 | elasticsearch-analysis-ik 107 | .pytest_cache 108 | example.json 109 | .vscode/ 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-Copilot 2 | 3 | Self-used nodes 4 | 5 | # Credits 6 | 7 | Code mostly adapted from below 8 | 9 | - https://github.com/bbc-mc/sdweb-eagle-pnginfo 10 | - https://github.com/twri/sdxl_prompt_styler 11 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .sdxl_prompt_styler import SDXLPromptStyler, SDXLPromptStylerAdvanced 2 | from .prompt_parser import parse_workflow 3 | import typing as t 4 | 5 | 6 | class BaseNode: 7 | CATEGORY = "copilot" 8 | 9 | @classmethod 10 | def INPUT_TYPES(cls): 11 | pass 12 | 13 | 14 | class EagleImageNode(BaseNode): 15 | """ 16 | A example node 17 | 18 | Class methods 19 | ------------- 20 | INPUT_TYPES (dict): 21 | Tell the main program input parameters of nodes. 22 | 23 | Attributes 24 | ---------- 25 | RETURN_TYPES (`tuple`): 26 | The type of each element in the output tulple. 27 | RETURN_NAMES (`tuple`): 28 | Optional: The name of each output in the output tulple. 29 | FUNCTION (`str`): 30 | The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() 31 | OUTPUT_NODE ([`bool`]): 32 | If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example. 33 | The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected. 34 | Assumed to be False if not present. 35 | CATEGORY (`str`): 36 | The category the node should appear in the UI. 37 | execute(s) -> tuple || None: 38 | The entry point method. The name of this method must be the same as the value of property `FUNCTION`. 39 | For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`. 40 | """ 41 | 42 | def __init__(self): 43 | pass 44 | 45 | @classmethod 46 | def INPUT_TYPES(s): 47 | """ 48 | Return a dictionary which contains config for all input fields. 49 | Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". 50 | Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. 51 | The type can be a list for selection. 52 | 53 | Returns: `dict`: 54 | - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` 55 | - Value input_fields (`dict`): Contains input fields config: 56 | * Key field_name (`string`): Name of a entry-point method's argument 57 | * Value field_config (`tuple`): 58 | + First value is a string indicate the type of field or a list for selection. 59 | + Secound value is a config for type "INT", "STRING" or "FLOAT". 60 | """ 61 | return { 62 | "required": { 63 | "image": ("IMAGE",), 64 | "int_field": ( 65 | "INT", 66 | { 67 | "default": 0, 68 | "min": 0, # Minimum value 69 | "max": 4096, # Maximum value 70 | "step": 64, # Slider's step 71 | }, 72 | ), 73 | "float_field": ( 74 | "FLOAT", 75 | {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}, 76 | ), 77 | "print_to_screen": (["enable", "disable"],), 78 | "string_field": ( 79 | "STRING", 80 | { 81 | "multiline": False, 82 | # True if you want the field to look like the one on the ClipTextEncode node 83 | "default": "Hello World!", 84 | }, 85 | ), 86 | }, 87 | "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, 88 | } 89 | 90 | RETURN_TYPES = () 91 | # RETURN_NAMES = ("image_output_name",) 92 | 93 | FUNCTION = "do_saving" 94 | 95 | def do_saving( 96 | self, 97 | image, 98 | string_field, 99 | int_field, 100 | float_field, 101 | print_to_screen, 102 | prompt=None, 103 | extra_pnginfo=None, 104 | ): 105 | print("--->imgae", "prompt", prompt, "extra", extra_pnginfo) 106 | if print_to_screen == "enable": 107 | print( 108 | f"""Your input contains: 109 | string_field aka input text: {string_field} 110 | int_field: {int_field} 111 | float_field: {float_field} 112 | """ 113 | ) 114 | # do some processing on the image, in this example I just invert it 115 | a = parse_workflow(prompt) 116 | print(a) 117 | 118 | 119 | TResolution = t.Literal[ 120 | "Square (1024x1024)", 121 | "Cinematic (1536x640)", 122 | "Cinematic (640x1536)", 123 | "Widescreen (1344x768)", 124 | "Widescreen (768x1344)", 125 | "Photo (1216x832)", 126 | "Photo (832x1216)", 127 | "Portrait (1152x896)", 128 | ] 129 | 130 | 131 | class SDXLResolutionPresets(BaseNode): 132 | RESOLUTIONS: list[TResolution] = [ 133 | "Square (1024x1024)", 134 | "Cinematic (1536x640)", 135 | "Cinematic (640x1536)", 136 | "Widescreen (1344x768)", 137 | "Widescreen (768x1344)", 138 | "Photo (1216x832)", 139 | "Photo (832x1216)", 140 | "Portrait (1152x896)", 141 | ] 142 | 143 | @classmethod 144 | def INPUT_TYPES(cls): 145 | return { 146 | "required": { 147 | "resolution": (cls.RESOLUTIONS, {"default": "Square (1024x1024)"}), 148 | }, 149 | } 150 | 151 | RETURN_TYPES = ("INT", "INT",) 152 | RETURN_NAMES = ("width", "height",) 153 | FUNCTION = "get_value" 154 | 155 | def get_value(self, resolution: TResolution, ) -> tuple[int, int]: 156 | if resolution == "Cinematic (1536x640)": 157 | return 1536, 640 158 | if resolution == "Cinematic (640x1536)": 159 | return 640, 1536 160 | if resolution == "Widescreen (1344x768)": 161 | return 1344, 768 162 | if resolution == "Widescreen (768x1344)": 163 | return 768, 1344 164 | if resolution == "Photo (1216x832)": 165 | return 1216, 832 166 | if resolution == "Photo (832x1216)": 167 | return 832, 1216 168 | if resolution == "Portrait (1152x896)": 169 | return 1152, 896 170 | if resolution == "Portrait (896x1152)": 171 | return 896, 1152 172 | if resolution == "Square (1024x1024)": 173 | return 1024, 1024 174 | 175 | 176 | # A dictionary that contains all nodes you want to export with their names 177 | # NOTE: names should be globally unique 178 | NODE_CLASS_MAPPINGS = { 179 | "EagleImageNode": EagleImageNode, 180 | "SDXLResolutionPresets": SDXLResolutionPresets, 181 | "SDXLPromptStyler": SDXLPromptStyler, 182 | "SDXLPromptStylerAdvanced": SDXLPromptStylerAdvanced, 183 | 184 | } 185 | 186 | # A dictionary that contains the friendly/humanly readable titles for the nodes 187 | NODE_DISPLAY_NAME_MAPPINGS = { 188 | "EagleImageNode": "Eagle Image Node for PNGInfo", 189 | "SDXLResolutionPresets": "SDXL Resolution Presets (ws)", 190 | "SDXLPromptStyler": "SDXL Prompt Styler", 191 | "SDXLPromptStylerAdvanced": "SDXL Prompt Styler Advanced", 192 | } 193 | -------------------------------------------------------------------------------- /data/sdxl_styles_base.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "base", 4 | "prompt": "{prompt}", 5 | "negative_prompt": "" 6 | } 7 | ] -------------------------------------------------------------------------------- /data/sdxl_styles_sai.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "sai-3d-model", 4 | "prompt": "professional 3d model {prompt} . octane render, highly detailed, volumetric, dramatic lighting", 5 | "negative_prompt": "ugly, deformed, noisy, low poly, blurry, painting" 6 | }, 7 | { 8 | "name": "sai-analog film", 9 | "prompt": "analog film photo {prompt} . faded film, desaturated, 35mm photo, grainy, vignette, vintage, Kodachrome, Lomography, stained, highly detailed, found footage", 10 | "negative_prompt": "painting, drawing, illustration, glitch, deformed, mutated, cross-eyed, ugly, disfigured" 11 | }, 12 | { 13 | "name": "sai-anime", 14 | "prompt": "anime artwork {prompt} . anime style, key visual, vibrant, studio anime, highly detailed", 15 | "negative_prompt": "photo, deformed, black and white, realism, disfigured, low contrast" 16 | }, 17 | { 18 | "name": "sai-cinematic", 19 | "prompt": "cinematic film still {prompt} . shallow depth of field, vignette, highly detailed, high budget, bokeh, cinemascope, moody, epic, gorgeous, film grain, grainy", 20 | "negative_prompt": "anime, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured" 21 | }, 22 | { 23 | "name": "sai-comic book", 24 | "prompt": "comic {prompt} . graphic illustration, comic art, graphic novel art, vibrant, highly detailed", 25 | "negative_prompt": "photograph, deformed, glitch, noisy, realistic, stock photo" 26 | }, 27 | { 28 | "name": "sai-craft clay", 29 | "prompt": "play-doh style {prompt} . sculpture, clay art, centered composition, Claymation", 30 | "negative_prompt": "sloppy, messy, grainy, highly detailed, ultra textured, photo" 31 | }, 32 | { 33 | "name": "sai-digital art", 34 | "prompt": "concept art {prompt} . digital artwork, illustrative, painterly, matte painting, highly detailed", 35 | "negative_prompt": "photo, photorealistic, realism, ugly" 36 | }, 37 | { 38 | "name": "sai-enhance", 39 | "prompt": "breathtaking {prompt} . award-winning, professional, highly detailed", 40 | "negative_prompt": "ugly, deformed, noisy, blurry, distorted, grainy" 41 | }, 42 | { 43 | "name": "sai-fantasy art", 44 | "prompt": "ethereal fantasy concept art of {prompt} . magnificent, celestial, ethereal, painterly, epic, majestic, magical, fantasy art, cover art, dreamy", 45 | "negative_prompt": "photographic, realistic, realism, 35mm film, dslr, cropped, frame, text, deformed, glitch, noise, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, sloppy, duplicate, mutated, black and white" 46 | }, 47 | { 48 | "name": "sai-isometric", 49 | "prompt": "isometric style {prompt} . vibrant, beautiful, crisp, detailed, ultra detailed, intricate", 50 | "negative_prompt": "deformed, mutated, ugly, disfigured, blur, blurry, noise, noisy, realistic, photographic" 51 | }, 52 | { 53 | "name": "sai-line art", 54 | "prompt": "line art drawing {prompt} . professional, sleek, modern, minimalist, graphic, line art, vector graphics", 55 | "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, blurry, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, mutated, realism, realistic, impressionism, expressionism, oil, acrylic" 56 | }, 57 | { 58 | "name": "sai-lowpoly", 59 | "prompt": "low-poly style {prompt} . low-poly game art, polygon mesh, jagged, blocky, wireframe edges, centered composition", 60 | "negative_prompt": "noisy, sloppy, messy, grainy, highly detailed, ultra textured, photo" 61 | }, 62 | { 63 | "name": "sai-neonpunk", 64 | "prompt": "neonpunk style {prompt} . cyberpunk, vaporwave, neon, vibes, vibrant, stunningly beautiful, crisp, detailed, sleek, ultramodern, magenta highlights, dark purple shadows, high contrast, cinematic, ultra detailed, intricate, professional", 65 | "negative_prompt": "painting, drawing, illustration, glitch, deformed, mutated, cross-eyed, ugly, disfigured" 66 | }, 67 | { 68 | "name": "sai-origami", 69 | "prompt": "origami style {prompt} . paper art, pleated paper, folded, origami art, pleats, cut and fold, centered composition", 70 | "negative_prompt": "noisy, sloppy, messy, grainy, highly detailed, ultra textured, photo" 71 | }, 72 | { 73 | "name": "sai-photographic", 74 | "prompt": "cinematic photo {prompt} . 35mm photograph, film, bokeh, professional, 4k, highly detailed", 75 | "negative_prompt": "drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly" 76 | }, 77 | { 78 | "name": "sai-pixel art", 79 | "prompt": "pixel-art {prompt} . low-res, blocky, pixel art style, 8-bit graphics", 80 | "negative_prompt": "sloppy, messy, blurry, noisy, highly detailed, ultra textured, photo, realistic" 81 | }, 82 | { 83 | "name": "sai-texture", 84 | "prompt": "texture {prompt} top down close-up", 85 | "negative_prompt": "ugly, deformed, noisy, blurry" 86 | } 87 | ] -------------------------------------------------------------------------------- /data/sdxl_styles_twri.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ads-advertising", 4 | "prompt": "advertising poster style {prompt} . Professional, modern, product-focused, commercial, eye-catching, highly detailed", 5 | "negative_prompt": "noisy, blurry, amateurish, sloppy, unattractive" 6 | }, 7 | { 8 | "name": "ads-automotive", 9 | "prompt": "automotive advertisement style {prompt} . sleek, dynamic, professional, commercial, vehicle-focused, high-resolution, highly detailed", 10 | "negative_prompt": "noisy, blurry, unattractive, sloppy, unprofessional" 11 | }, 12 | { 13 | "name": "ads-corporate", 14 | "prompt": "corporate branding style {prompt} . professional, clean, modern, sleek, minimalist, business-oriented, highly detailed", 15 | "negative_prompt": "noisy, blurry, grungy, sloppy, cluttered, disorganized" 16 | }, 17 | { 18 | "name": "ads-fashion editorial", 19 | "prompt": "fashion editorial style {prompt} . high fashion, trendy, stylish, editorial, magazine style, professional, highly detailed", 20 | "negative_prompt": "outdated, blurry, noisy, unattractive, sloppy" 21 | }, 22 | { 23 | "name": "ads-food photography", 24 | "prompt": "food photography style {prompt} . appetizing, professional, culinary, high-resolution, commercial, highly detailed", 25 | "negative_prompt": "unappetizing, sloppy, unprofessional, noisy, blurry" 26 | }, 27 | { 28 | "name": "ads-gourmet food photography", 29 | "prompt": "gourmet food photo of {prompt} . soft natural lighting, macro details, vibrant colors, fresh ingredients, glistening textures, bokeh background, styled plating, wooden tabletop, garnished, tantalizing, editorial quality", 30 | "negative_prompt": "cartoon, anime, sketch, grayscale, dull, overexposed, cluttered, messy plate, deformed" 31 | }, 32 | { 33 | "name": "ads-luxury", 34 | "prompt": "luxury product style {prompt} . elegant, sophisticated, high-end, luxurious, professional, highly detailed", 35 | "negative_prompt": "cheap, noisy, blurry, unattractive, amateurish" 36 | }, 37 | { 38 | "name": "ads-real estate", 39 | "prompt": "real estate photography style {prompt} . professional, inviting, well-lit, high-resolution, property-focused, commercial, highly detailed", 40 | "negative_prompt": "dark, blurry, unappealing, noisy, unprofessional" 41 | }, 42 | { 43 | "name": "ads-retail", 44 | "prompt": "retail packaging style {prompt} . vibrant, enticing, commercial, product-focused, eye-catching, professional, highly detailed", 45 | "negative_prompt": "noisy, blurry, amateurish, sloppy, unattractive" 46 | }, 47 | { 48 | "name": "artstyle-abstract", 49 | "prompt": "abstract style {prompt} . non-representational, colors and shapes, expression of feelings, imaginative, highly detailed", 50 | "negative_prompt": "realistic, photographic, figurative, concrete" 51 | }, 52 | { 53 | "name": "artstyle-abstract expressionism", 54 | "prompt": "abstract expressionist painting {prompt} . energetic brushwork, bold colors, abstract forms, expressive, emotional", 55 | "negative_prompt": "realistic, photorealistic, low contrast, plain, simple, monochrome" 56 | }, 57 | { 58 | "name": "artstyle-art deco", 59 | "prompt": "art deco style {prompt} . geometric shapes, bold colors, luxurious, elegant, decorative, symmetrical, ornate, detailed", 60 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, modernist, minimalist" 61 | }, 62 | { 63 | "name": "artstyle-art nouveau", 64 | "prompt": "art nouveau style {prompt} . elegant, decorative, curvilinear forms, nature-inspired, ornate, detailed", 65 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, modernist, minimalist" 66 | }, 67 | { 68 | "name": "artstyle-constructivist", 69 | "prompt": "constructivist style {prompt} . geometric shapes, bold colors, dynamic composition, propaganda art style", 70 | "negative_prompt": "realistic, photorealistic, low contrast, plain, simple, abstract expressionism" 71 | }, 72 | { 73 | "name": "artstyle-cubist", 74 | "prompt": "cubist artwork {prompt} . geometric shapes, abstract, innovative, revolutionary", 75 | "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, low contrast, noisy" 76 | }, 77 | { 78 | "name": "artstyle-expressionist", 79 | "prompt": "expressionist {prompt} . raw, emotional, dynamic, distortion for emotional effect, vibrant, use of unusual colors, detailed", 80 | "negative_prompt": "realism, symmetry, quiet, calm, photo" 81 | }, 82 | { 83 | "name": "artstyle-graffiti", 84 | "prompt": "graffiti style {prompt} . street art, vibrant, urban, detailed, tag, mural", 85 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic" 86 | }, 87 | { 88 | "name": "artstyle-hyperrealism", 89 | "prompt": "hyperrealistic art {prompt} . extremely high-resolution details, photographic, realism pushed to extreme, fine texture, incredibly lifelike", 90 | "negative_prompt": "simplified, abstract, unrealistic, impressionistic, low resolution" 91 | }, 92 | { 93 | "name": "artstyle-impressionist", 94 | "prompt": "impressionist painting {prompt} . loose brushwork, vibrant color, light and shadow play, captures feeling over form", 95 | "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, low contrast, noisy" 96 | }, 97 | { 98 | "name": "artstyle-pointillism", 99 | "prompt": "pointillism style {prompt} . composed entirely of small, distinct dots of color, vibrant, highly detailed", 100 | "negative_prompt": "line drawing, smooth shading, large color fields, simplistic" 101 | }, 102 | { 103 | "name": "artstyle-pop art", 104 | "prompt": "pop Art style {prompt} . bright colors, bold outlines, popular culture themes, ironic or kitsch", 105 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, minimalist" 106 | }, 107 | { 108 | "name": "artstyle-psychedelic", 109 | "prompt": "psychedelic style {prompt} . vibrant colors, swirling patterns, abstract forms, surreal, trippy", 110 | "negative_prompt": "monochrome, black and white, low contrast, realistic, photorealistic, plain, simple" 111 | }, 112 | { 113 | "name": "artstyle-renaissance", 114 | "prompt": "renaissance style {prompt} . realistic, perspective, light and shadow, religious or mythological themes, highly detailed", 115 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, modernist, minimalist, abstract" 116 | }, 117 | { 118 | "name": "artstyle-steampunk", 119 | "prompt": "steampunk style {prompt} . antique, mechanical, brass and copper tones, gears, intricate, detailed", 120 | "negative_prompt": "deformed, glitch, noisy, low contrast, anime, photorealistic" 121 | }, 122 | { 123 | "name": "artstyle-surrealist", 124 | "prompt": "surrealist art {prompt} . dreamlike, mysterious, provocative, symbolic, intricate, detailed", 125 | "negative_prompt": "anime, photorealistic, realistic, deformed, glitch, noisy, low contrast" 126 | }, 127 | { 128 | "name": "artstyle-typography", 129 | "prompt": "typographic art {prompt} . stylized, intricate, detailed, artistic, text-based", 130 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic" 131 | }, 132 | { 133 | "name": "artstyle-watercolor", 134 | "prompt": "watercolor painting {prompt} . vibrant, beautiful, painterly, detailed, textural, artistic", 135 | "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, low contrast, noisy" 136 | }, 137 | { 138 | "name": "futuristic-biomechanical", 139 | "prompt": "biomechanical style {prompt} . blend of organic and mechanical elements, futuristic, cybernetic, detailed, intricate", 140 | "negative_prompt": "natural, rustic, primitive, organic, simplistic" 141 | }, 142 | { 143 | "name": "futuristic-biomechanical cyberpunk", 144 | "prompt": "biomechanical cyberpunk {prompt} . cybernetics, human-machine fusion, dystopian, organic meets artificial, dark, intricate, highly detailed", 145 | "negative_prompt": "natural, colorful, deformed, sketch, low contrast, watercolor" 146 | }, 147 | { 148 | "name": "futuristic-cybernetic", 149 | "prompt": "cybernetic style {prompt} . futuristic, technological, cybernetic enhancements, robotics, artificial intelligence themes", 150 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, historical, medieval" 151 | }, 152 | { 153 | "name": "futuristic-cybernetic robot", 154 | "prompt": "cybernetic robot {prompt} . android, AI, machine, metal, wires, tech, futuristic, highly detailed", 155 | "negative_prompt": "organic, natural, human, sketch, watercolor, low contrast" 156 | }, 157 | { 158 | "name": "futuristic-cyberpunk cityscape", 159 | "prompt": "cyberpunk cityscape {prompt} . neon lights, dark alleys, skyscrapers, futuristic, vibrant colors, high contrast, highly detailed", 160 | "negative_prompt": "natural, rural, deformed, low contrast, black and white, sketch, watercolor" 161 | }, 162 | { 163 | "name": "futuristic-futuristic", 164 | "prompt": "futuristic style {prompt} . sleek, modern, ultramodern, high tech, detailed", 165 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, vintage, antique" 166 | }, 167 | { 168 | "name": "futuristic-retro cyberpunk", 169 | "prompt": "retro cyberpunk {prompt} . 80's inspired, synthwave, neon, vibrant, detailed, retro futurism", 170 | "negative_prompt": "modern, desaturated, black and white, realism, low contrast" 171 | }, 172 | { 173 | "name": "futuristic-retro futurism", 174 | "prompt": "retro-futuristic {prompt} . vintage sci-fi, 50s and 60s style, atomic age, vibrant, highly detailed", 175 | "negative_prompt": "contemporary, realistic, rustic, primitive" 176 | }, 177 | { 178 | "name": "futuristic-sci-fi", 179 | "prompt": "sci-fi style {prompt} . futuristic, technological, alien worlds, space themes, advanced civilizations", 180 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, historical, medieval" 181 | }, 182 | { 183 | "name": "futuristic-vaporwave", 184 | "prompt": "vaporwave style {prompt} . retro aesthetic, cyberpunk, vibrant, neon colors, vintage 80s and 90s style, highly detailed", 185 | "negative_prompt": "monochrome, muted colors, realism, rustic, minimalist, dark" 186 | }, 187 | { 188 | "name": "game-bubble bobble", 189 | "prompt": "Bubble Bobble style {prompt} . 8-bit, cute, pixelated, fantasy, vibrant, reminiscent of Bubble Bobble game", 190 | "negative_prompt": "realistic, modern, photorealistic, violent, horror" 191 | }, 192 | { 193 | "name": "game-cyberpunk game", 194 | "prompt": "cyberpunk game style {prompt} . neon, dystopian, futuristic, digital, vibrant, detailed, high contrast, reminiscent of cyberpunk genre video games", 195 | "negative_prompt": "historical, natural, rustic, low detailed" 196 | }, 197 | { 198 | "name": "game-fighting game", 199 | "prompt": "fighting game style {prompt} . dynamic, vibrant, action-packed, detailed character design, reminiscent of fighting video games", 200 | "negative_prompt": "peaceful, calm, minimalist, photorealistic" 201 | }, 202 | { 203 | "name": "game-gta", 204 | "prompt": "GTA-style artwork {prompt} . satirical, exaggerated, pop art style, vibrant colors, iconic characters, action-packed", 205 | "negative_prompt": "realistic, black and white, low contrast, impressionist, cubist, noisy, blurry, deformed" 206 | }, 207 | { 208 | "name": "game-mario", 209 | "prompt": "Super Mario style {prompt} . vibrant, cute, cartoony, fantasy, playful, reminiscent of Super Mario series", 210 | "negative_prompt": "realistic, modern, horror, dystopian, violent" 211 | }, 212 | { 213 | "name": "game-minecraft", 214 | "prompt": "Minecraft style {prompt} . blocky, pixelated, vibrant colors, recognizable characters and objects, game assets", 215 | "negative_prompt": "smooth, realistic, detailed, photorealistic, noise, blurry, deformed" 216 | }, 217 | { 218 | "name": "game-pokemon", 219 | "prompt": "Pokémon style {prompt} . vibrant, cute, anime, fantasy, reminiscent of Pokémon series", 220 | "negative_prompt": "realistic, modern, horror, dystopian, violent" 221 | }, 222 | { 223 | "name": "game-retro arcade", 224 | "prompt": "retro arcade style {prompt} . 8-bit, pixelated, vibrant, classic video game, old school gaming, reminiscent of 80s and 90s arcade games", 225 | "negative_prompt": "modern, ultra-high resolution, photorealistic, 3D" 226 | }, 227 | { 228 | "name": "game-retro game", 229 | "prompt": "retro game art {prompt} . 16-bit, vibrant colors, pixelated, nostalgic, charming, fun", 230 | "negative_prompt": "realistic, photorealistic, 35mm film, deformed, glitch, low contrast, noisy" 231 | }, 232 | { 233 | "name": "game-rpg fantasy game", 234 | "prompt": "role-playing game (RPG) style fantasy {prompt} . detailed, vibrant, immersive, reminiscent of high fantasy RPG games", 235 | "negative_prompt": "sci-fi, modern, urban, futuristic, low detailed" 236 | }, 237 | { 238 | "name": "game-strategy game", 239 | "prompt": "strategy game style {prompt} . overhead view, detailed map, units, reminiscent of real-time strategy video games", 240 | "negative_prompt": "first-person view, modern, photorealistic" 241 | }, 242 | { 243 | "name": "game-streetfighter", 244 | "prompt": "Street Fighter style {prompt} . vibrant, dynamic, arcade, 2D fighting game, highly detailed, reminiscent of Street Fighter series", 245 | "negative_prompt": "3D, realistic, modern, photorealistic, turn-based strategy" 246 | }, 247 | { 248 | "name": "game-zelda", 249 | "prompt": "Legend of Zelda style {prompt} . vibrant, fantasy, detailed, epic, heroic, reminiscent of The Legend of Zelda series", 250 | "negative_prompt": "sci-fi, modern, realistic, horror" 251 | }, 252 | { 253 | "name": "misc-architectural", 254 | "prompt": "architectural style {prompt} . clean lines, geometric shapes, minimalist, modern, architectural drawing, highly detailed", 255 | "negative_prompt": "curved lines, ornate, baroque, abstract, grunge" 256 | }, 257 | { 258 | "name": "misc-disco", 259 | "prompt": "disco-themed {prompt} . vibrant, groovy, retro 70s style, shiny disco balls, neon lights, dance floor, highly detailed", 260 | "negative_prompt": "minimalist, rustic, monochrome, contemporary, simplistic" 261 | }, 262 | { 263 | "name": "misc-dreamscape", 264 | "prompt": "dreamscape {prompt} . surreal, ethereal, dreamy, mysterious, fantasy, highly detailed", 265 | "negative_prompt": "realistic, concrete, ordinary, mundane" 266 | }, 267 | { 268 | "name": "misc-dystopian", 269 | "prompt": "dystopian style {prompt} . bleak, post-apocalyptic, somber, dramatic, highly detailed", 270 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, cheerful, optimistic, vibrant, colorful" 271 | }, 272 | { 273 | "name": "misc-fairy tale", 274 | "prompt": "fairy tale {prompt} . magical, fantastical, enchanting, storybook style, highly detailed", 275 | "negative_prompt": "realistic, modern, ordinary, mundane" 276 | }, 277 | { 278 | "name": "misc-gothic", 279 | "prompt": "gothic style {prompt} . dark, mysterious, haunting, dramatic, ornate, detailed", 280 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, cheerful, optimistic" 281 | }, 282 | { 283 | "name": "misc-grunge", 284 | "prompt": "grunge style {prompt} . textured, distressed, vintage, edgy, punk rock vibe, dirty, noisy", 285 | "negative_prompt": "smooth, clean, minimalist, sleek, modern, photorealistic" 286 | }, 287 | { 288 | "name": "misc-horror", 289 | "prompt": "horror-themed {prompt} . eerie, unsettling, dark, spooky, suspenseful, grim, highly detailed", 290 | "negative_prompt": "cheerful, bright, vibrant, light-hearted, cute" 291 | }, 292 | { 293 | "name": "misc-kawaii", 294 | "prompt": "kawaii style {prompt} . cute, adorable, brightly colored, cheerful, anime influence, highly detailed", 295 | "negative_prompt": "dark, scary, realistic, monochrome, abstract" 296 | }, 297 | { 298 | "name": "misc-lovecraftian", 299 | "prompt": "lovecraftian horror {prompt} . eldritch, cosmic horror, unknown, mysterious, surreal, highly detailed", 300 | "negative_prompt": "light-hearted, mundane, familiar, simplistic, realistic" 301 | }, 302 | { 303 | "name": "misc-macabre", 304 | "prompt": "macabre style {prompt} . dark, gothic, grim, haunting, highly detailed", 305 | "negative_prompt": "bright, cheerful, light-hearted, cartoonish, cute" 306 | }, 307 | { 308 | "name": "misc-manga", 309 | "prompt": "manga style {prompt} . vibrant, high-energy, detailed, iconic, Japanese comic style", 310 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, Western comic style" 311 | }, 312 | { 313 | "name": "misc-metropolis", 314 | "prompt": "metropolis-themed {prompt} . urban, cityscape, skyscrapers, modern, futuristic, highly detailed", 315 | "negative_prompt": "rural, natural, rustic, historical, simple" 316 | }, 317 | { 318 | "name": "misc-minimalist", 319 | "prompt": "minimalist style {prompt} . simple, clean, uncluttered, modern, elegant", 320 | "negative_prompt": "ornate, complicated, highly detailed, cluttered, disordered, messy, noisy" 321 | }, 322 | { 323 | "name": "misc-monochrome", 324 | "prompt": "monochrome {prompt} . black and white, contrast, tone, texture, detailed", 325 | "negative_prompt": "colorful, vibrant, noisy, blurry, deformed" 326 | }, 327 | { 328 | "name": "misc-nautical", 329 | "prompt": "nautical-themed {prompt} . sea, ocean, ships, maritime, beach, marine life, highly detailed", 330 | "negative_prompt": "landlocked, desert, mountains, urban, rustic" 331 | }, 332 | { 333 | "name": "misc-space", 334 | "prompt": "space-themed {prompt} . cosmic, celestial, stars, galaxies, nebulas, planets, science fiction, highly detailed", 335 | "negative_prompt": "earthly, mundane, ground-based, realism" 336 | }, 337 | { 338 | "name": "misc-stained glass", 339 | "prompt": "stained glass style {prompt} . vibrant, beautiful, translucent, intricate, detailed", 340 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic" 341 | }, 342 | { 343 | "name": "misc-techwear fashion", 344 | "prompt": "techwear fashion {prompt} . futuristic, cyberpunk, urban, tactical, sleek, dark, highly detailed", 345 | "negative_prompt": "vintage, rural, colorful, low contrast, realism, sketch, watercolor" 346 | }, 347 | { 348 | "name": "misc-tribal", 349 | "prompt": "tribal style {prompt} . indigenous, ethnic, traditional patterns, bold, natural colors, highly detailed", 350 | "negative_prompt": "modern, futuristic, minimalist, pastel" 351 | }, 352 | { 353 | "name": "misc-zentangle", 354 | "prompt": "zentangle {prompt} . intricate, abstract, monochrome, patterns, meditative, highly detailed", 355 | "negative_prompt": "colorful, representative, simplistic, large fields of color" 356 | }, 357 | { 358 | "name": "papercraft-collage", 359 | "prompt": "collage style {prompt} . mixed media, layered, textural, detailed, artistic", 360 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic" 361 | }, 362 | { 363 | "name": "papercraft-flat papercut", 364 | "prompt": "flat papercut style {prompt} . silhouette, clean cuts, paper, sharp edges, minimalist, color block", 365 | "negative_prompt": "3D, high detail, noise, grainy, blurry, painting, drawing, photo, disfigured" 366 | }, 367 | { 368 | "name": "papercraft-kirigami", 369 | "prompt": "kirigami representation of {prompt} . 3D, paper folding, paper cutting, Japanese, intricate, symmetrical, precision, clean lines", 370 | "negative_prompt": "painting, drawing, 2D, noisy, blurry, deformed" 371 | }, 372 | { 373 | "name": "papercraft-paper mache", 374 | "prompt": "paper mache representation of {prompt} . 3D, sculptural, textured, handmade, vibrant, fun", 375 | "negative_prompt": "2D, flat, photo, sketch, digital art, deformed, noisy, blurry" 376 | }, 377 | { 378 | "name": "papercraft-paper quilling", 379 | "prompt": "paper quilling art of {prompt} . intricate, delicate, curling, rolling, shaping, coiling, loops, 3D, dimensional, ornamental", 380 | "negative_prompt": "photo, painting, drawing, 2D, flat, deformed, noisy, blurry" 381 | }, 382 | { 383 | "name": "papercraft-papercut collage", 384 | "prompt": "papercut collage of {prompt} . mixed media, textured paper, overlapping, asymmetrical, abstract, vibrant", 385 | "negative_prompt": "photo, 3D, realistic, drawing, painting, high detail, disfigured" 386 | }, 387 | { 388 | "name": "papercraft-papercut shadow box", 389 | "prompt": "3D papercut shadow box of {prompt} . layered, dimensional, depth, silhouette, shadow, papercut, handmade, high contrast", 390 | "negative_prompt": "painting, drawing, photo, 2D, flat, high detail, blurry, noisy, disfigured" 391 | }, 392 | { 393 | "name": "papercraft-stacked papercut", 394 | "prompt": "stacked papercut art of {prompt} . 3D, layered, dimensional, depth, precision cut, stacked layers, papercut, high contrast", 395 | "negative_prompt": "2D, flat, noisy, blurry, painting, drawing, photo, deformed" 396 | }, 397 | { 398 | "name": "papercraft-thick layered papercut", 399 | "prompt": "thick layered papercut art of {prompt} . deep 3D, volumetric, dimensional, depth, thick paper, high stack, heavy texture, tangible layers", 400 | "negative_prompt": "2D, flat, thin paper, low stack, smooth texture, painting, drawing, photo, deformed" 401 | }, 402 | { 403 | "name": "photo-alien", 404 | "prompt": "alien-themed {prompt} . extraterrestrial, cosmic, otherworldly, mysterious, sci-fi, highly detailed", 405 | "negative_prompt": "earthly, mundane, common, realistic, simple" 406 | }, 407 | { 408 | "name": "photo-film noir", 409 | "prompt": "film noir style {prompt} . monochrome, high contrast, dramatic shadows, 1940s style, mysterious, cinematic", 410 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, vibrant, colorful" 411 | }, 412 | { 413 | "name": "photo-glamour", 414 | "prompt": "glamorous photo {prompt} . high fashion, luxurious, extravagant, stylish, sensual, opulent, elegance, stunning beauty, professional, high contrast, detailed", 415 | "negative_prompt": "ugly, deformed, noisy, blurry, distorted, grainy, sketch, low contrast, dull, plain, modest" 416 | }, 417 | { 418 | "name": "photo-hdr", 419 | "prompt": "HDR photo of {prompt} . High dynamic range, vivid, rich details, clear shadows and highlights, realistic, intense, enhanced contrast, highly detailed", 420 | "negative_prompt": "flat, low contrast, oversaturated, underexposed, overexposed, blurred, noisy" 421 | }, 422 | { 423 | "name": "photo-iphone photographic", 424 | "prompt": "iphone photo {prompt} . large depth of field, deep depth of field, highly detailed", 425 | "negative_prompt": "drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly, shallow depth of field, bokeh" 426 | }, 427 | { 428 | "name": "photo-long exposure", 429 | "prompt": "long exposure photo of {prompt} . Blurred motion, streaks of light, surreal, dreamy, ghosting effect, highly detailed", 430 | "negative_prompt": "static, noisy, deformed, shaky, abrupt, flat, low contrast" 431 | }, 432 | { 433 | "name": "photo-neon noir", 434 | "prompt": "neon noir {prompt} . cyberpunk, dark, rainy streets, neon signs, high contrast, low light, vibrant, highly detailed", 435 | "negative_prompt": "bright, sunny, daytime, low contrast, black and white, sketch, watercolor" 436 | }, 437 | { 438 | "name": "photo-silhouette", 439 | "prompt": "silhouette style {prompt} . high contrast, minimalistic, black and white, stark, dramatic", 440 | "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, color, realism, photorealistic" 441 | }, 442 | { 443 | "name": "photo-tilt-shift", 444 | "prompt": "tilt-shift photo of {prompt} . selective focus, miniature effect, blurred background, highly detailed, vibrant, perspective control", 445 | "negative_prompt": "blurry, noisy, deformed, flat, low contrast, unrealistic, oversaturated, underexposed" 446 | } 447 | ] -------------------------------------------------------------------------------- /eagleapi/api_application.py: -------------------------------------------------------------------------------- 1 | # seealso: https://api.eagle.cool/application/info 2 | # 3 | import requests 4 | 5 | from . import api_util 6 | 7 | 8 | def info(server_url="http://localhost", port=41595, timeout_connect=3, timeout_read=10): 9 | """EAGLE API:/api/application/info 10 | 11 | Returns: 12 | Response: return of requests.post 13 | """ 14 | 15 | API_URL = f"{server_url}:{port}/api/application/info" 16 | 17 | try: 18 | r_get = requests.get(API_URL, timeout=(timeout_connect, timeout_read)) 19 | except requests.exceptions.Timeout as e: 20 | print("Error: api_application.info") 21 | print(e) 22 | return 23 | 24 | return r_get 25 | 26 | 27 | # 28 | # Support function 29 | # 30 | def is_alive( 31 | server_url="http://localhost", port=41595, timeout_connect=3, timeout_read=10 32 | ): 33 | if not port or type(port) != int or port == "": 34 | port = 41595 35 | try: 36 | r_get = info(server_url, port, timeout_connect, timeout_read) 37 | except Exception as e: 38 | print("Error: api_application.is_alive") 39 | print(e) 40 | return False 41 | try: 42 | r_get.raise_for_status() 43 | return True 44 | except: 45 | return False 46 | 47 | 48 | def is_valid_url_port(server_url_port="", timeout_connect=3, timeout_read=3): 49 | if not server_url_port or server_url_port == "": 50 | return False 51 | server_url, port = api_util.get_url_port(server_url_port) 52 | if not server_url or not port: 53 | return False 54 | if not is_alive( 55 | server_url=server_url, 56 | port=port, 57 | timeout_connect=timeout_connect, 58 | timeout_read=timeout_read, 59 | ): 60 | return False 61 | return True 62 | -------------------------------------------------------------------------------- /eagleapi/api_folder.py: -------------------------------------------------------------------------------- 1 | # see also https://api.eagle.cool/folder/list 2 | # 3 | import requests 4 | import sys 5 | 6 | from . import api_util 7 | 8 | 9 | def create( 10 | newfoldername, 11 | server_url="http://localhost", 12 | port=41595, 13 | allow_duplicate_name=True, 14 | timeout_connect=3, 15 | timeout_read=10, 16 | ): 17 | """EAGLE API:/api/folder/list 18 | 19 | Method: POST 20 | 21 | Returns: 22 | list(response dict): return list of response.json() 23 | """ 24 | API_URL = f"{server_url}:{port}/api/folder/create" 25 | 26 | def _init_data(newfoldername): 27 | _data = {} 28 | if newfoldername and newfoldername != "": 29 | _data.update({"folderName": newfoldername}) 30 | return _data 31 | 32 | data = _init_data(newfoldername) 33 | 34 | # check duplicate if needed 35 | if not allow_duplicate_name: 36 | r_post = list() 37 | _ret = api_util.findFolderByName(r_post, newfoldername) 38 | if _ret != None or len(_ret) > 0: 39 | print( 40 | f'ERROR: create folder with same name is forbidden by option. [eagleapi.folder.create] foldername="{newfoldername}"', 41 | file=sys.stderr, 42 | ) 43 | return 44 | 45 | r_post = requests.post(API_URL, json=data, timeout=(timeout_connect, timeout_read)) 46 | return r_post 47 | 48 | 49 | def rename( 50 | folderId, 51 | newName, 52 | server_url="http://localhost", 53 | port=41595, 54 | timeout_connect=3, 55 | timeout_read=10, 56 | ): 57 | """EAGLE API:/api/folder/rename 58 | 59 | Method: POST 60 | 61 | Returns: 62 | list(response dict): return list of response.json() 63 | """ 64 | data = {"folderId": folderId, "newName": newName} 65 | API_URL = f"{server_url}:{port}/api/folder/rename" 66 | r_post = requests.post(API_URL, json=data, timeout=(timeout_connect, timeout_read)) 67 | return r_post 68 | 69 | 70 | def list(server_url="http://localhost", port=41595, timeout_connect=3, timeout_read=10): 71 | """EAGLE API:/api/folder/list 72 | 73 | Method: GET 74 | 75 | Returns: 76 | Response: return of requests.post 77 | """ 78 | 79 | API_URL = f"{server_url}:{port}/api/folder/list" 80 | 81 | r_get = requests.get(API_URL, timeout=(timeout_connect, timeout_read)) 82 | 83 | return r_get 84 | -------------------------------------------------------------------------------- /eagleapi/api_item.py: -------------------------------------------------------------------------------- 1 | # seealso: https://api.eagle.cool/item/add-from-path 2 | # seealso: https://api.eagle.cool/item/add-from-paths 3 | # 4 | import requests 5 | import os 6 | 7 | import base64 8 | 9 | DEBUG = False 10 | 11 | 12 | def dprint(str): 13 | if DEBUG: 14 | print(str) 15 | 16 | 17 | class EAGLE_ITEM_PATH: 18 | def __init__( 19 | self, filefullpath, filename="", website="", tags: list = [], annotation="" 20 | ): 21 | """Data container for addFromPath, addFromPaths 22 | 23 | Args: 24 | filefullpath : Required, full path of the local files. 25 | filename : (option), name of image to be added. 26 | website : (option), address of the source of the image. 27 | tags (list) : (option), tags for the image. 28 | annotation : (option), annotation for the image. 29 | """ 30 | self.filefullpath = filefullpath 31 | self.filename = filename 32 | self.website = website 33 | self.tags = tags 34 | self.annotation = annotation 35 | 36 | def output_data(self): 37 | """ 38 | output data in json format for POST 39 | """ 40 | _data = { 41 | "path": self.filefullpath, 42 | "name": os.path.splitext(os.path.basename(self.filefullpath))[0] 43 | if (self.filename == None or self.filename == "") 44 | else self.filename, 45 | } 46 | if self.website and self.website != "": 47 | _data.update({"website": self.website}) 48 | if self.tags and len(self.tags) > 0: 49 | _data.update({"tags": self.tags}) 50 | if self.annotation and self.annotation != "": 51 | _data.update({"annotation": self.annotation}) 52 | return _data 53 | 54 | 55 | class EAGLE_ITEM_URL: 56 | def __init__( 57 | self, 58 | url, 59 | name, 60 | website="", 61 | tags=[], 62 | annotation="", 63 | modificationTime="", 64 | folderId="", 65 | headers={}, 66 | ): 67 | """Data container for addFromURL, addFromURLs 68 | url : Required, the URL of the image to be added. Supports http, https, base64 69 | name : Required, The name of the image to be added. 70 | website : The Address of the source of the image 71 | tags: Tags for the image. 72 | annotation: The annotation for the image. 73 | modificationTime: The creation date of the image. The parameter can be used to alter the image's sorting order in Eagle. 74 | headers: Optional, customize the HTTP headers properties, this could be used to circumvent the security of certain websites. 75 | 76 | folderId: If this parameter is defined, the image will be added to the corresponding folder. 77 | """ 78 | self.url = url 79 | self.name = name 80 | self.website = website 81 | self.tags = tags 82 | self.annotation = annotation 83 | self.modificationTime = modificationTime 84 | self.folderId = folderId 85 | self.headers = headers 86 | 87 | def convert_file_to_base64url(self, filepath=None): 88 | if not filepath or filepath == "": 89 | if self.url and self.url != "": 90 | filepath = self.url 91 | else: 92 | print("Error convert_file_to_base64url: invalid filepath") 93 | return filepath 94 | else: 95 | self.url = filepath 96 | if not os.path.exists(filepath): 97 | print("Error convert_file_to_base64url: file not found.") 98 | return filepath 99 | try: 100 | with open(filepath, "rb") as file: 101 | enc_file = base64.urlsafe_b64encode(file.read()) 102 | self.url = f"data:image/png;base64, {enc_file.decode('utf-8')}" 103 | except Exception as e: 104 | print("Error convert_file_to_base64url: eocode failed") 105 | print(e) 106 | return filepath 107 | 108 | return self.url 109 | 110 | def output_data(self): 111 | """ 112 | output data in json format for POST 113 | """ 114 | _data = {"url": self.url, "name": self.name} 115 | # add optional data 116 | if self.website and self.website != "": 117 | _data.update({"website": self.website}) 118 | if self.tags and len(self.tags) > 0: 119 | _data.update({"tags": self.tags}) 120 | if self.annotation and self.annotation != "": 121 | _data.update({"annotation": self.annotation}) 122 | if self.modificationTime and self.modificationTime != "": 123 | _data.update({"modificationTime": self.modificationTime}) 124 | if self.folderId and self.folderId != "": 125 | _data.update({"folderId": self.folderId}) 126 | if self.headers and len(self.headers) > 0: 127 | _data.update({"headers": self.headers}) 128 | return _data 129 | 130 | 131 | def add_from_URL( 132 | item: EAGLE_ITEM_URL, folderId=None, server_url="http://localhost", port=41595 133 | ): 134 | API_URL = f"{server_url}:{port}/api/item/addFromURL" 135 | _data = item.output_data() 136 | if folderId and folderId != "": 137 | _data.update({"folderId": folderId}) 138 | r_post = requests.post(API_URL, json=_data) 139 | return r_post 140 | 141 | 142 | def add_from_URL_base64( 143 | item: EAGLE_ITEM_URL, folderId=None, server_url="http://localhost", port=41595 144 | ): 145 | API_URL = f"{server_url}:{port}/api/item/addFromURL" 146 | item.url = item.convert_file_to_base64url() 147 | _data = item.output_data() 148 | if folderId and folderId != "": 149 | _data.update({"folderId": folderId}) 150 | r_post = requests.post(API_URL, json=_data) 151 | return r_post 152 | 153 | 154 | def add_from_path( 155 | item: EAGLE_ITEM_PATH, folderId=None, server_url="http://localhost", port=41595 156 | ): 157 | API_URL = f"{server_url}:{port}/api/item/addFromPath" 158 | _data = item.output_data() 159 | if folderId and folderId != "": 160 | _data.update({"folderId": folderId}) 161 | r_post = requests.post(API_URL, json=_data) 162 | return r_post 163 | 164 | 165 | def add_from_paths( 166 | files, folderId=None, server_url="http://localhost", port=41595, step=None 167 | ): 168 | """EAGLE API:/api/item/addFromPaths 169 | 170 | Method: POST 171 | 172 | Args: 173 | path: Required, the path of the local files. 174 | name: Required, the name of images to be added. 175 | website: The Address of the source of the images. 176 | annotation: The annotation for the images. 177 | tags: Tags for the images. 178 | folderId: If this parameter is defined, the image will be added to the corresponding folder. 179 | step: interval image num of doing POST. Defaults is None (disabled) 180 | 181 | Returns: 182 | Response: return of requests.posts 183 | """ 184 | API_URL = f"{server_url}:{port}/api/item/addFromPaths" 185 | 186 | if step: 187 | step = int(step) 188 | 189 | def _init_data(): 190 | _data = {"items": []} 191 | if folderId and folderId != "": 192 | _data.update({"folderId": folderId}) 193 | return _data 194 | 195 | r_posts = [] 196 | data = _init_data() 197 | for _index, _item in enumerate(files): 198 | _item: EAGLE_ITEM_PATH = _item 199 | _data = _item.output_data() 200 | if _data: 201 | data["items"].append(_data) 202 | if step and step > 0: 203 | if ((_index + 1) - ((_index + 1) // step) * step) == 0: 204 | _ret = requests.post(API_URL, json=data) 205 | try: 206 | r_posts.append(_ret.json()) 207 | except: 208 | r_posts.append(_ret) 209 | data = _init_data() 210 | if (len(data["items"]) > 0) or (not step or step <= 0): 211 | _ret = requests.post(API_URL, json=data) 212 | try: 213 | r_posts.append(_ret.json()) 214 | except: 215 | r_posts.append(_ret) 216 | 217 | return [x for x in r_posts if x != ""] 218 | -------------------------------------------------------------------------------- /eagleapi/api_util.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import ipaddress 3 | from urllib.parse import urlparse 4 | 5 | from . import api_folder 6 | 7 | 8 | def get_url_port(server_url_port=""): 9 | if not server_url_port or server_url_port == "": 10 | return None, None 11 | o = urlparse(server_url_port) 12 | _url = f"http://{o.hostname}" 13 | if o.hostname != "localhost": 14 | _ip = ipaddress.ip_address(o.hostname) 15 | if _ip.version == 6: 16 | _url = f"http://[{o.hostname}]" 17 | port = o.port 18 | return _url, port 19 | 20 | 21 | # util for /api/folder/list 22 | def findFolderByID(r_posts, target_id): 23 | return findFolderByName(r_posts, target_id, findByID=True) 24 | 25 | 26 | def findFolderByName(r_posts, target_name, findByID=False): 27 | _ret = [] 28 | if not target_name or target_name == "" or not r_posts: 29 | return None 30 | _all_folder = getAllFolder(r_posts) 31 | for _data in _all_folder: 32 | if (findByID and _data.get("id", "") == target_name) or ( 33 | _data.get("name", "") == target_name 34 | ): 35 | _ret = _data 36 | break 37 | return _ret 38 | 39 | 40 | def getAllFolder(r_posts): 41 | """get dict of {"folderId": _data, ...""" 42 | 43 | def dig_folder(data, dig_count, dig_limit=10): 44 | dig_count += 1 45 | if dig_count > dig_limit: 46 | return [] 47 | _ret = [data] 48 | if "children" in data and len(data["children"]) > 0: 49 | for _child in data["children"]: 50 | _ret += dig_folder(_child, dig_count) 51 | return _ret 52 | 53 | _ret = [] 54 | if not r_posts: 55 | return None 56 | _posts = r_posts.json() 57 | if not _posts or "status" not in _posts or _posts["status"] != "success": 58 | return None 59 | if "data" in _posts and len(_posts["data"]) > 0: 60 | for _data in _posts["data"]: 61 | _ret += dig_folder(_data, 0) 62 | return _ret 63 | 64 | 65 | # 66 | # Support functions 67 | # 68 | 69 | 70 | def print_response(_res: requests.Response): 71 | print(f"status code : {_res.status_code}") 72 | print(f"headers : {_res.headers}") 73 | print(f"text : {_res.text}") 74 | print(f"encoding : {_res.encoding}") 75 | print(f"cookies : {_res.cookies}") 76 | 77 | print(f"content : {_res.content}") 78 | print(f"content decode: {_res.content.decode(encoding=_res.apparent_encoding)}") 79 | 80 | 81 | def get_json_from_response(_res: requests.Response): 82 | try: 83 | _result = _res.json() 84 | return _result 85 | except: 86 | return _res 87 | 88 | 89 | def find_or_create_folder( 90 | folder_name_or_id, 91 | allow_create_new_folder=False, 92 | server_url="http://localhost", 93 | port=41595, 94 | timeout_connect=3, 95 | timeout_read=10, 96 | ): 97 | """ 98 | Find or Create folder on Eagle, by folderId or FolderName 99 | 100 | Args: 101 | folder_name_or_id (str): 102 | allow_create_new_folder (bool, optional): if True, create new folder on Eagle. Defaults to False. 103 | server_url (str, optional): Defaults to "http://localhost". 104 | port (int, optional): Defaults to 41595. 105 | timeout_connect (int, optional): Defaults to 3. 106 | timeout_read (int, optional): Defaults to 10. 107 | Return: 108 | folderId or "" 109 | 110 | """ 111 | _eagle_folderid = "" 112 | if folder_name_or_id and folder_name_or_id != "": 113 | _ret_folder_list = api_folder.list( 114 | server_url=server_url, 115 | port=port, 116 | timeout_connect=timeout_connect, 117 | timeout_read=timeout_read, 118 | ) 119 | 120 | # serach by name 121 | _ret = findFolderByName(_ret_folder_list, folder_name_or_id) 122 | if _ret and len(_ret) > 0: 123 | _eagle_folderid = _ret.get("id", "") 124 | # serach by ID 125 | if _eagle_folderid == "": 126 | _ret = findFolderByID(_ret_folder_list, folder_name_or_id) 127 | if _ret and len(_ret) > 0: 128 | _eagle_folderid = _ret.get("id", "") 129 | if _eagle_folderid == "": 130 | if allow_create_new_folder: # allow new 131 | _r_get = api_folder.create( 132 | folder_name_or_id, 133 | server_url=server_url, 134 | port=port, 135 | timeout_connect=timeout_connect, 136 | timeout_read=timeout_read, 137 | ) 138 | try: 139 | _eagle_folderid = _r_get.json().get("data").get("id") 140 | except: 141 | _eagle_folderid = "" 142 | return _eagle_folderid 143 | -------------------------------------------------------------------------------- /prompt_parser.py: -------------------------------------------------------------------------------- 1 | def trace_inputs(node, workflow, path=None, tags=None): 2 | if tags is None: 3 | tags = [] 4 | 5 | class_type = node["class_type"] 6 | inputs = node["inputs"] 7 | 8 | # 添加类类型到路径中 9 | if path is None: 10 | path = [class_type] 11 | else: 12 | path.append(class_type) 13 | 14 | for key, value in inputs.items(): 15 | new_path = path + [key] 16 | if isinstance(value, list): 17 | ref_id, _ = value 18 | ref_node = workflow[str(ref_id)] 19 | trace_inputs(ref_node, workflow, new_path, tags) 20 | else: 21 | tag = f"{'->'.join(new_path[-4:])}::{value}" 22 | tags.append(tag) 23 | 24 | return tags 25 | 26 | 27 | def parse_workflow(workflow): 28 | # TODO: only parse from eagle node!!! 29 | image_nodes = [ 30 | node for node in workflow.values() if node["class_type"] == "PreviewImage" 31 | ] 32 | image_node = image_nodes[0] 33 | tags = trace_inputs(image_node, workflow) 34 | print("Image Node Tags:") 35 | tags = list(set(tags)) 36 | tags.sort() 37 | # // raw info 38 | return tags 39 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-copilot" 3 | description = "" 4 | version = "1.0.0" 5 | license = "LICENSE" 6 | 7 | [project.urls] 8 | Repository = "https://github.com/hylarucoder/comfyui-copilot" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "" 13 | DisplayName = "comfyui-copilot" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /sdxl_prompt_styler.py: -------------------------------------------------------------------------------- 1 | # https://github.com/twri/sdxl_prompt_styler/blob/main/sdxl_prompt_styler.py 2 | import json 3 | import os 4 | 5 | 6 | def read_json_file(file_path): 7 | """ 8 | Reads a JSON file's content and returns it. 9 | Ensures content matches the expected format. 10 | """ 11 | if not os.access(file_path, os.R_OK): 12 | print(f"Warning: No read permissions for file {file_path}") 13 | return None 14 | 15 | try: 16 | with open(file_path, 'r', encoding='utf-8') as file: 17 | content = json.load(file) 18 | # Check if the content matches the expected format. 19 | if not all(['name' in item and 'prompt' in item and 'negative_prompt' in item for item in content]): 20 | print(f"Warning: Invalid content in file {file_path}") 21 | return None 22 | return content 23 | except Exception as e: 24 | print(f"An error occurred while reading {file_path}: {str(e)}") 25 | return None 26 | 27 | 28 | def read_sdxl_styles(json_data): 29 | """ 30 | Returns style names from the provided JSON data. 31 | """ 32 | if not isinstance(json_data, list): 33 | print("Error: input data must be a list") 34 | return [] 35 | 36 | return [item['name'] for item in json_data if isinstance(item, dict) and 'name' in item] 37 | 38 | 39 | def get_all_json_files(directory): 40 | """ 41 | Returns all JSON files from the specified directory. 42 | """ 43 | return [os.path.join(directory, file) for file in os.listdir(directory) if 44 | file.endswith('.json') and os.path.isfile(os.path.join(directory, file))] 45 | 46 | 47 | def load_styles_from_directory(directory): 48 | """ 49 | Loads styles from all JSON files in the directory. 50 | Renames duplicate style names by appending a suffix. 51 | """ 52 | json_files = get_all_json_files(directory) 53 | combined_data = [] 54 | seen = set() 55 | 56 | for json_file in json_files: 57 | json_data = read_json_file(json_file) 58 | if json_data: 59 | for item in json_data: 60 | original_style = item['name'] 61 | style = original_style 62 | suffix = 1 63 | while style in seen: 64 | style = f"{original_style}_{suffix}" 65 | suffix += 1 66 | item['name'] = style 67 | seen.add(style) 68 | combined_data.append(item) 69 | 70 | unique_style_names = [item['name'] for item in combined_data if isinstance(item, dict) and 'name' in item] 71 | 72 | return combined_data, unique_style_names 73 | 74 | 75 | def validate_json_data(json_data): 76 | """ 77 | Validates the structure of the JSON data. 78 | """ 79 | if not isinstance(json_data, list): 80 | return False 81 | for template in json_data: 82 | if 'name' not in template or 'prompt' not in template: 83 | return False 84 | return True 85 | 86 | 87 | def find_template_by_name(json_data, template_name): 88 | """ 89 | Returns a template from the JSON data by name or None if not found. 90 | """ 91 | for template in json_data: 92 | if template['name'] == template_name: 93 | return template 94 | return None 95 | 96 | 97 | def split_template_advanced(template: str) -> tuple: 98 | """ 99 | Splits a template into two parts based on a specific pattern. 100 | """ 101 | if "{prompt} ." in template: 102 | template_prompt_g, template_prompt_l = template.split("{prompt} .", 1) 103 | template_prompt_g = template_prompt_g.strip() + " {prompt}" 104 | template_prompt_l = template_prompt_l.strip() 105 | else: 106 | template_prompt_g = template 107 | template_prompt_l = "" 108 | 109 | return template_prompt_g, template_prompt_l 110 | 111 | 112 | def replace_prompts_in_template(template, positive_prompt, negative_prompt): 113 | """ 114 | Replace the placeholders in a given template with the provided prompts. 115 | 116 | Args: 117 | - template (dict): The template containing prompt placeholders. 118 | - positive_prompt (str): The positive prompt to replace '{prompt}' in the template. 119 | - negative_prompt (str): The negative prompt to be combined with any existing negative prompt in the template. 120 | 121 | Returns: 122 | - tuple: A tuple containing the replaced positive and negative prompts. 123 | """ 124 | positive_result = template['prompt'].replace('{prompt}', positive_prompt) 125 | 126 | json_negative_prompt = template.get('negative_prompt', "") 127 | negative_result = f"{json_negative_prompt}, {negative_prompt}" if json_negative_prompt and negative_prompt else json_negative_prompt or negative_prompt 128 | 129 | return positive_result, negative_result 130 | 131 | 132 | def replace_prompts_in_template_advanced(template, positive_prompt_g, positive_prompt_l, negative_prompt, 133 | negative_prompt_to): 134 | """ 135 | Replace the placeholders in a given template with the provided prompts and split them accordingly. 136 | 137 | Args: 138 | - template (dict): The template containing prompt placeholders. 139 | - positive_prompt_g (str): The main positive prompt to replace '{prompt}' in the template. 140 | - positive_prompt_l (str): The auxiliary positive prompt to be combined in a specific manner. 141 | - negative_prompt (str): The negative prompt to be combined with any existing negative prompt in the template. 142 | - negative_prompt_to (str): The negative prompt destination {Both, G only, L only}. 143 | 144 | Returns: 145 | - tuple: A tuple containing the replaced main positive, auxiliary positive, combined positive, main negative, auxiliary negative, and negative prompts. 146 | """ 147 | template_prompt_g, template_prompt_l_template = split_template_advanced(template['prompt']) 148 | 149 | text_g_positive = template_prompt_g.replace("{prompt}", positive_prompt_g) 150 | 151 | text_l_positive = f"{template_prompt_l_template}, {positive_prompt_l}" if template_prompt_l_template and positive_prompt_l else template_prompt_l_template or positive_prompt_l 152 | 153 | text_positive = f"{text_g_positive} . {text_l_positive}" if text_l_positive else text_g_positive 154 | 155 | json_negative_prompt = template.get('negative_prompt', "") 156 | text_negative = f"{json_negative_prompt}, {negative_prompt}" if json_negative_prompt and negative_prompt else json_negative_prompt or negative_prompt 157 | 158 | text_g_negative = "" 159 | if negative_prompt_to in ("Both", "G only"): 160 | text_g_negative = text_negative 161 | 162 | text_l_negative = "" 163 | if negative_prompt_to in ("Both", "L only"): 164 | text_l_negative = text_negative 165 | 166 | return text_g_positive, text_l_positive, text_positive, text_g_negative, text_l_negative, text_negative 167 | 168 | 169 | def read_sdxl_templates_replace_and_combine(json_data, template_name, positive_prompt, negative_prompt): 170 | """ 171 | Find a specific template by its name, then replace and combine its placeholders with the provided prompts. 172 | 173 | Args: 174 | - json_data (list): The list of templates. 175 | - template_name (str): The name of the desired template. 176 | - positive_prompt (str): The positive prompt to replace placeholders. 177 | - negative_prompt (str): The negative prompt to be combined. 178 | 179 | Returns: 180 | - tuple: A tuple containing the replaced and combined positive and negative prompts. 181 | """ 182 | if not validate_json_data(json_data): 183 | return positive_prompt, negative_prompt 184 | 185 | template = find_template_by_name(json_data, template_name) 186 | 187 | if template: 188 | return replace_prompts_in_template(template, positive_prompt, negative_prompt) 189 | else: 190 | return positive_prompt, negative_prompt 191 | 192 | 193 | def read_sdxl_templates_replace_and_combine_advanced(json_data, template_name, positive_prompt_g, positive_prompt_l, 194 | negative_prompt, negative_prompt_to): 195 | """ 196 | Find a specific template by its name, then replace and combine its placeholders with the provided prompts in an advanced manner. 197 | 198 | Args: 199 | - json_data (list): The list of templates. 200 | - template_name (str): The name of the desired template. 201 | - positive_prompt_g (str): The main positive prompt. 202 | - positive_prompt_l (str): The auxiliary positive prompt. 203 | - negative_prompt (str): The negative prompt to be combined. 204 | - negative_prompt_to (str): The negative prompt destination {Both, G only, L only}. 205 | 206 | Returns: 207 | - tuple: A tuple containing the replaced and combined main positive, auxiliary positive, combined positive, main negative, auxiliary negative, and negative prompts. 208 | """ 209 | if not validate_json_data(json_data): 210 | return positive_prompt_g, positive_prompt_l, f"{positive_prompt_g} . {positive_prompt_l}", negative_prompt, negative_prompt, negative_prompt 211 | 212 | template = find_template_by_name(json_data, template_name) 213 | 214 | if template: 215 | return replace_prompts_in_template_advanced(template, positive_prompt_g, positive_prompt_l, negative_prompt, 216 | negative_prompt_to) 217 | else: 218 | return positive_prompt_g, positive_prompt_l, f"{positive_prompt_g} . {positive_prompt_l}", negative_prompt, negative_prompt, negative_prompt 219 | 220 | 221 | class SDXLPromptStyler: 222 | 223 | def __init__(self): 224 | pass 225 | 226 | @classmethod 227 | def INPUT_TYPES(self): 228 | current_directory = os.path.dirname(os.path.realpath(__file__)) + "/data" 229 | self.json_data, styles = load_styles_from_directory(current_directory) 230 | 231 | return { 232 | "required": { 233 | "text_positive": ("STRING", {"default": "", "multiline": True}), 234 | "text_negative": ("STRING", {"default": "", "multiline": True}), 235 | "style": (styles,), 236 | "log_prompt": (["No", "Yes"], {"default": "No"}), 237 | }, 238 | } 239 | 240 | RETURN_TYPES = ('STRING', 'STRING',) 241 | RETURN_NAMES = ('text_positive', 'text_negative',) 242 | FUNCTION = 'prompt_styler' 243 | CATEGORY = 'utils' 244 | 245 | def prompt_styler(self, text_positive, text_negative, style, log_prompt): 246 | # Process and combine prompts in templates 247 | # The function replaces the positive prompt placeholder in the template, 248 | # and combines the negative prompt with the template's negative prompt, if they exist. 249 | text_positive_styled, text_negative_styled = read_sdxl_templates_replace_and_combine(self.json_data, style, 250 | text_positive, 251 | text_negative) 252 | 253 | # If logging is enabled (log_prompt is set to "Yes"), 254 | # print the style, positive and negative text, and positive and negative prompts to the console 255 | if log_prompt == "Yes": 256 | print(f"style: {style}") 257 | print(f"text_positive: {text_positive}") 258 | print(f"text_negative: {text_negative}") 259 | print(f"text_positive_styled: {text_positive_styled}") 260 | print(f"text_negative_styled: {text_negative_styled}") 261 | 262 | return text_positive_styled, text_negative_styled 263 | 264 | 265 | class SDXLPromptStylerAdvanced: 266 | 267 | def __init__(self): 268 | pass 269 | 270 | @classmethod 271 | def INPUT_TYPES(self): 272 | current_directory = os.path.dirname(os.path.realpath(__file__)) + "/data" 273 | self.json_data, styles = load_styles_from_directory(current_directory) 274 | 275 | return { 276 | "required": { 277 | "text_positive_g": ("STRING", {"default": "", "multiline": True}), 278 | "text_positive_l": ("STRING", {"default": "", "multiline": True}), 279 | "text_negative": ("STRING", {"default": "", "multiline": True}), 280 | "style": (styles,), 281 | "negative_prompt_to": (["Both", "G only", "L only"], {"default": "Both"}), 282 | "log_prompt": (["No", "Yes"], {"default": "No"}), 283 | }, 284 | } 285 | 286 | RETURN_TYPES = ('STRING', 'STRING', 'STRING', 'STRING', 'STRING', 'STRING',) 287 | RETURN_NAMES = ( 288 | 'text_positive_g', 'text_positive_l', 'text_positive', 'text_negative_g', 'text_negative_l', 'text_negative',) 289 | FUNCTION = 'prompt_styler_advanced' 290 | CATEGORY = 'utils' 291 | 292 | def prompt_styler_advanced(self, text_positive_g, text_positive_l, text_negative, style, negative_prompt_to, 293 | log_prompt): 294 | # Process and combine prompts in templates 295 | # The function replaces the positive prompt placeholder in the template, 296 | # and combines the negative prompt with the template's negative prompt, if they exist. 297 | text_positive_g_styled, text_positive_l_styled, text_positive_styled, text_negative_g_styled, text_negative_l_styled, text_negative_styled = read_sdxl_templates_replace_and_combine_advanced( 298 | self.json_data, style, text_positive_g, text_positive_l, text_negative, negative_prompt_to) 299 | 300 | # If logging is enabled (log_prompt is set to "Yes"), 301 | # print the style, positive and negative text, and positive and negative prompts to the console 302 | if log_prompt == "Yes": 303 | print(f"style: {style}") 304 | print(f"text_positive_g: {text_positive_g}") 305 | print(f"text_positive_l: {text_positive_l}") 306 | print(f"text_negative: {text_negative}") 307 | print(f"text_positive_g_styled: {text_positive_g_styled}") 308 | print(f"text_positive_l_styled: {text_positive_l_styled}") 309 | print(f"text_positive_styled: {text_positive_styled}") 310 | print(f"text_negative_g_styled: {text_negative_g_styled}") 311 | print(f"text_negative_l_styled: {text_negative_l_styled}") 312 | print(f"text_negative_styled: {text_negative_styled}") 313 | 314 | return text_positive_g_styled, text_positive_l_styled, text_positive_styled, text_negative_g_styled, text_negative_l_styled, text_negative_styled 315 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/comfyui-copilot/03e82a0df73433ae5941c9288f80aca1f4159455/tests/__init__.py -------------------------------------------------------------------------------- /tests/examples/01.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "inputs": { 4 | "seed": 979537337409677, 5 | "steps": 30, 6 | "cfg": 8.0, 7 | "sampler_name": "ddim", 8 | "scheduler": "normal", 9 | "denoise": 1.0, 10 | "model": ["4", 0], 11 | "positive": ["6", 0], 12 | "negative": ["7", 0], 13 | "latent_image": ["5", 0] 14 | }, 15 | "class_type": "KSampler" 16 | }, 17 | "4": { 18 | "inputs": { "ckpt_name": "AWPainting 1.1_v1.1.safetensors" }, 19 | "class_type": "CheckpointLoaderSimple" 20 | }, 21 | "5": { 22 | "inputs": { "width": 512, "height": 512, "batch_size": 1 }, 23 | "class_type": "EmptyLatentImage" 24 | }, 25 | "6": { 26 | "inputs": { 27 | "text": "masterpiece, best quality,\\n1girl, solo, long hair, black hair, from behind", 28 | "clip": ["4", 1] 29 | }, 30 | "class_type": "CLIPTextEncode" 31 | }, 32 | "7": { 33 | "inputs": { 34 | "text": "(worst quality, low quality:1.4), EasyNegative, ng_deepnegative_v1_75t, bad_prompt_version2,", 35 | "clip": ["4", 1] 36 | }, 37 | "class_type": "CLIPTextEncode" 38 | }, 39 | "8": { 40 | "inputs": { "samples": ["3", 0], "vae": ["4", 2] }, 41 | "class_type": "VAEDecode" 42 | }, 43 | "9": { 44 | "inputs": { "filename_prefix": "ComfyUI", "images": ["11", 0] }, 45 | "class_type": "SaveImage" 46 | }, 47 | "11": { 48 | "inputs": { 49 | "int_field": 0, 50 | "float_field": 1.0, 51 | "print_to_screen": "enable", 52 | "string_field": "Hello World!", 53 | "image": ["8", 0] 54 | }, 55 | "class_type": "EagleImageNode" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/examples/02.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "inputs": { 4 | "seed": 804645408561758, 5 | "steps": 40, 6 | "cfg": 1.5, 7 | "sampler_name": "dpmpp_2m_sde", 8 | "scheduler": "karras", 9 | "denoise": 1, 10 | "model": ["47", 0], 11 | "positive": ["32", 0], 12 | "negative": ["33", 0], 13 | "latent_image": ["5", 0] 14 | }, 15 | "class_type": "KSampler" 16 | }, 17 | "4": { 18 | "inputs": { 19 | "ckpt_name": "agelesnate_v121.safetensors" 20 | }, 21 | "class_type": "CheckpointLoaderSimple" 22 | }, 23 | "5": { 24 | "inputs": { 25 | "width": 768, 26 | "height": 512, 27 | "batch_size": 1 28 | }, 29 | "class_type": "EmptyLatentImage" 30 | }, 31 | "8": { 32 | "inputs": { 33 | "samples": ["3", 0], 34 | "vae": ["34", 0] 35 | }, 36 | "class_type": "VAEDecode" 37 | }, 38 | "17": { 39 | "inputs": { 40 | "seed": 804645408561758, 41 | "steps": 20, 42 | "cfg": 7, 43 | "sampler_name": "dpmpp_2m_sde", 44 | "scheduler": "karras", 45 | "denoise": 0.5, 46 | "model": ["47", 0], 47 | "positive": ["32", 0], 48 | "negative": ["33", 0], 49 | "latent_image": ["25", 0] 50 | }, 51 | "class_type": "KSampler" 52 | }, 53 | "21": { 54 | "inputs": { 55 | "samples": ["17", 0], 56 | "vae": ["34", 0] 57 | }, 58 | "class_type": "VAEDecode" 59 | }, 60 | "24": { 61 | "inputs": { 62 | "upscale_model": ["26", 0], 63 | "image": ["8", 0] 64 | }, 65 | "class_type": "ImageUpscaleWithModel" 66 | }, 67 | "25": { 68 | "inputs": { 69 | "pixels": ["27", 0], 70 | "vae": ["34", 0] 71 | }, 72 | "class_type": "VAEEncode" 73 | }, 74 | "26": { 75 | "inputs": { 76 | "model_name": "RealESRGAN_x4plus_anime_6B.pth" 77 | }, 78 | "class_type": "UpscaleModelLoader" 79 | }, 80 | "27": { 81 | "inputs": { 82 | "upscale_method": "area", 83 | "scale_by": 0.5, 84 | "image": ["24", 0] 85 | }, 86 | "class_type": "ImageScaleBy" 87 | }, 88 | "32": { 89 | "inputs": { 90 | "text": "1girl, solo, expressionless, silver hair, ponytail, purple eyes, face mask, ninja, blue bikini, medium breats, cleavage, scarf,", 91 | "clip": ["47", 1] 92 | }, 93 | "class_type": "CLIPTextEncode" 94 | }, 95 | "33": { 96 | "inputs": { 97 | "text": "embeddings:EasyNegative, (worst quality, low quality, normal quality, bad anatomy:1.4), text, watermark,", 98 | "clip": ["47", 1] 99 | }, 100 | "class_type": "CLIPTextEncode" 101 | }, 102 | "34": { 103 | "inputs": { 104 | "vae_name": "vae-ft-mse-840000-ema-pruned.safetensors" 105 | }, 106 | "class_type": "VAELoader" 107 | }, 108 | "47": { 109 | "inputs": { 110 | "lora_name": "oc_v10.safetensors", 111 | "strength_model": 0.5, 112 | "strength_clip": 0.5, 113 | "model": ["4", 0], 114 | "clip": ["4", 1] 115 | }, 116 | "class_type": "LoraLoader" 117 | }, 118 | "71": { 119 | "inputs": { 120 | "images": ["21", 0] 121 | }, 122 | "class_type": "PreviewImage" 123 | }, 124 | "91": { 125 | "inputs": { 126 | "lossless_webp": "lossy", 127 | "compression": 80, 128 | "positive_prompt": "1girl, solo, expressionless, silver hair, ponytail, purple eyes, face mask, ninja, blue bikini, medium breats, cleavage, scarf,", 129 | "negative_prompt": "embeddings:EasyNegative, (worst quality, low quality, normal quality, bad anatomy:1.4), text, watermark,", 130 | "annotation": "", 131 | "images": ["21", 0] 132 | }, 133 | "class_type": "Send Webp Image to Eagle" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/examples/03-SDXL.json: -------------------------------------------------------------------------------- 1 | { 2 | "3": { 3 | "inputs": { 4 | "seed": 234288240797552, 5 | "steps": 40, 6 | "cfg": 8, 7 | "sampler_name": "dpmpp_2m_sde", 8 | "scheduler": "karras", 9 | "denoise": 1, 10 | "model": ["4", 0], 11 | "positive": ["32", 0], 12 | "negative": ["33", 0], 13 | "latent_image": ["5", 0] 14 | }, 15 | "class_type": "KSampler" 16 | }, 17 | "4": { 18 | "inputs": { 19 | "ckpt_name": "sdXL_v10VAEFix.safetensors" 20 | }, 21 | "class_type": "CheckpointLoaderSimple" 22 | }, 23 | "5": { 24 | "inputs": { 25 | "width": 832, 26 | "height": 1216, 27 | "batch_size": 1 28 | }, 29 | "class_type": "EmptyLatentImage" 30 | }, 31 | "8": { 32 | "inputs": { 33 | "samples": ["3", 0], 34 | "vae": ["34", 0] 35 | }, 36 | "class_type": "VAEDecode" 37 | }, 38 | "12": { 39 | "inputs": { 40 | "ascore": 6, 41 | "width": 2048, 42 | "height": 2048, 43 | "text": "photo of beautiful age 18 girl, pastel hair, freckles sexy, beautiful, close up, young, dslr, 8k, 4k, ultrarealistic, realistic, natural skin, textured skin", 44 | "clip": ["30", 1] 45 | }, 46 | "class_type": "CLIPTextEncodeSDXLRefiner" 47 | }, 48 | "16": { 49 | "inputs": { 50 | "ascore": 6, 51 | "width": 2048, 52 | "height": 2048, 53 | "text": "prompt: text, watermark, low quality, medium quality, blurry, censored, wrinkles, deformed, mutated text, watermark, low quality, medium quality, blurry, censored, wrinkles, deformed, mutated", 54 | "clip": ["30", 1] 55 | }, 56 | "class_type": "CLIPTextEncodeSDXLRefiner" 57 | }, 58 | "17": { 59 | "inputs": { 60 | "seed": 0, 61 | "steps": 20, 62 | "cfg": 8, 63 | "sampler_name": "euler_ancestral", 64 | "scheduler": "normal", 65 | "denoise": 0.3000000000000001, 66 | "model": ["30", 0], 67 | "positive": ["12", 0], 68 | "negative": ["16", 0], 69 | "latent_image": ["3", 0] 70 | }, 71 | "class_type": "KSampler" 72 | }, 73 | "21": { 74 | "inputs": { 75 | "samples": ["17", 0], 76 | "vae": ["34", 0] 77 | }, 78 | "class_type": "VAEDecode" 79 | }, 80 | "28": { 81 | "inputs": { 82 | "filename_prefix": "xl_output", 83 | "images": ["8", 0] 84 | }, 85 | "class_type": "SaveImage" 86 | }, 87 | "30": { 88 | "inputs": { 89 | "ckpt_name": "sd_xl_refiner_1.0.safetensors" 90 | }, 91 | "class_type": "CheckpointLoaderSimple" 92 | }, 93 | "32": { 94 | "inputs": { 95 | "text": "photo of beautiful age 18 girl, pastel hair, freckles sexy, beautiful, close up, young, dslr, 8k, 4k, ultrarealistic, realistic, natural skin, textured skin", 96 | "clip": ["4", 1] 97 | }, 98 | "class_type": "CLIPTextEncode" 99 | }, 100 | "33": { 101 | "inputs": { 102 | "text": "prompt: text, watermark, low quality, medium quality, blurry, censored, wrinkles, deformed, mutated text, watermark, low quality, medium quality, blurry, censored, wrinkles, deformed, mutated", 103 | "clip": ["4", 1] 104 | }, 105 | "class_type": "CLIPTextEncode" 106 | }, 107 | "34": { 108 | "inputs": { 109 | "vae_name": "sdxl_vae.safetensors" 110 | }, 111 | "class_type": "VAELoader" 112 | }, 113 | "35": { 114 | "inputs": { 115 | "lossless_webp": "lossy", 116 | "compression": 80, 117 | "positive_prompt": "", 118 | "negative_prompt": "", 119 | "annotation": "", 120 | "images": ["21", 0] 121 | }, 122 | "class_type": "Send Webp Image to Eagle" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/test_prompt_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from prompt_parser import parse_workflow 5 | 6 | current_path = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | 9 | def test_prompt_parser_02(): 10 | path = f"{current_path}/examples/02.json" 11 | prompt_info = open(path, "rt").read() 12 | parsed = parse_workflow(json.loads(prompt_info)) 13 | assert parsed == [ 14 | "CLIPTextEncode->clip->LoraLoader->lora_name::oc_v10.safetensors", 15 | "CLIPTextEncode->clip->LoraLoader->strength_clip::0.5", 16 | "CLIPTextEncode->clip->LoraLoader->strength_model::0.5", 17 | "ImageUpscaleWithModel->upscale_model->UpscaleModelLoader->model_name::RealESRGAN_x4plus_anime_6B.pth", 18 | "KSampler->latent_image->EmptyLatentImage->batch_size::1", 19 | "KSampler->latent_image->EmptyLatentImage->height::512", 20 | "KSampler->latent_image->EmptyLatentImage->width::768", 21 | "KSampler->model->LoraLoader->lora_name::oc_v10.safetensors", 22 | "KSampler->model->LoraLoader->strength_clip::0.5", 23 | "KSampler->model->LoraLoader->strength_model::0.5", 24 | "KSampler->negative->CLIPTextEncode->text::embeddings:EasyNegative, (worst quality, low quality, normal quality, bad anatomy:1.4), text, watermark,", 25 | "KSampler->positive->CLIPTextEncode->text::1girl, solo, expressionless, silver hair, ponytail, purple eyes, face mask, ninja, blue bikini, medium breats, cleavage, scarf,", 26 | "LoraLoader->clip->CheckpointLoaderSimple->ckpt_name::agelesnate_v121.safetensors", 27 | "LoraLoader->model->CheckpointLoaderSimple->ckpt_name::agelesnate_v121.safetensors", 28 | "VAEDecode->samples->KSampler->cfg::1.5", 29 | "VAEDecode->samples->KSampler->cfg::7", 30 | "VAEDecode->samples->KSampler->denoise::0.5", 31 | "VAEDecode->samples->KSampler->denoise::1", 32 | "VAEDecode->samples->KSampler->sampler_name::dpmpp_2m_sde", 33 | "VAEDecode->samples->KSampler->scheduler::karras", 34 | "VAEDecode->samples->KSampler->seed::804645408561758", 35 | "VAEDecode->samples->KSampler->steps::20", 36 | "VAEDecode->samples->KSampler->steps::40", 37 | "VAEDecode->vae->VAELoader->vae_name::vae-ft-mse-840000-ema-pruned.safetensors", 38 | "VAEEncode->pixels->ImageScaleBy->scale_by::0.5", 39 | "VAEEncode->pixels->ImageScaleBy->upscale_method::area", 40 | "VAEEncode->vae->VAELoader->vae_name::vae-ft-mse-840000-ema-pruned.safetensors", 41 | ] 42 | --------------------------------------------------------------------------------