From bb70e01985a8cf1e702b0d54d4a58b437bd0f14a Mon Sep 17 00:00:00 2001 From: "Eric W. Burns" Date: Mon, 10 Feb 2025 22:21:51 -0500 Subject: [PATCH] initial commit initial commit --- LICENSE | 4 +- __init__.py | 3 + color_data.py | 316 ++++ flux_example_ebu_prompthelper.json | 2562 ++++++++++++++++++++++++++ make_palette_analogous.py | 170 ++ make_palette_art_house.py | 74 + make_palette_chaotic.py | 14 + make_palette_complementary.py | 149 ++ make_palette_double_complementary.py | 112 ++ make_palette_modified_compound.py | 149 ++ make_palette_split_complementary.py | 220 +++ make_palette_triadic.py | 221 +++ nodes.py | 833 +++++++++ pyproject.toml | 16 + readme.md | 194 ++ requirements.txt | 1 + weather_utils.py | 319 ++++ 17 files changed, 5355 insertions(+), 2 deletions(-) create mode 100644 __init__.py create mode 100644 color_data.py create mode 100644 flux_example_ebu_prompthelper.json create mode 100644 make_palette_analogous.py create mode 100644 make_palette_art_house.py create mode 100644 make_palette_chaotic.py create mode 100644 make_palette_complementary.py create mode 100644 make_palette_double_complementary.py create mode 100644 make_palette_modified_compound.py create mode 100644 make_palette_split_complementary.py create mode 100644 make_palette_triadic.py create mode 100644 nodes.py create mode 100644 pyproject.toml create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 weather_utils.py diff --git a/LICENSE b/LICENSE index cf3094f..1f7a4c7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Eric W. Burns +Copyright (c) 2025 Eric Burns Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d721463 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/color_data.py b/color_data.py new file mode 100644 index 0000000..376e584 --- /dev/null +++ b/color_data.py @@ -0,0 +1,316 @@ +# color_data.py + +# Master COLORS dictionary +COLORS = { + # Reds + 'CRIMSON': '#DC143C', + 'CADMIUM RED': '#E30022', + 'CADMIUM RED DEEP': '#B31B1B', + 'PYRROLE RED': '#FF3A2A', + 'SCARLET': '#FF2400', + 'CARMINE': '#960018', + 'MAGENTA': '#FF00FF', + 'VENETIAN RED': '#C80815', + 'INDIAN RED': '#CD5C5C', + 'RUBY RED': '#9B111E', + 'TRUE RED': '#FF0000', + + # Pinks + 'POWDER PINK': '#FFB6C1', + 'STRAWBERRY PINK': '#FF9899', + 'HOT PINK': '#FF69B4', + 'PERSIAN ROSE': '#FE28A2', + 'FLAMINGO PINK': '#FC8EAC', + 'ROSE BLUSH': '#FFE4E1', + 'BALLET PINK': '#F4C2C2', + 'BLUSH ROSE': '#FEC5E5', + 'FUCHSIA': '#FF00FF', + 'MAGENTA': '#FF00FF', + 'ELECTRIC PINK': '#FF1493', + 'COTTON CANDY PINK': '#FFB7D5', + + # Oranges + 'CADMIUM ORANGE': '#FF6103', + 'BURNT ORANGE': '#CC5500', + 'PERSIMMON': '#EC5800', + 'SUNSET ORANGE': '#FD5E53', + 'APRICOT ORANGE': '#FFA500', + 'PASTEL ORANGE': '#FFB347', + 'BURNT SIENNA': '#E97451', + 'RAW SIENNA': '#D68A59', + 'TANGERINE': '#FFA500', + 'ORANGE CREAM': '#FFD8B1', + 'MANGO': '#FFA62B', + + # Yellows + 'GOLD OCHRE': '#D4AF37', + 'YELLOW OCHRE': '#DFAF2C', + 'NAPLES YELLOW': '#FADA5E', + 'INDIAN YELLOW': '#E3A857', + 'AZO YELLOW': '#FAFA33', + 'HANSA YELLOW': '#E9D66B', + 'CANARY YELLOW': '#FFEF00', + 'MARIGOLD': '#EAA221', + 'AMBER': '#FFBF00', + 'WARM YELLOW': '#FFAA33', + 'LEMON YELLOW': '#FFF44F', + 'PASTEL YELLOW': '#FFF9A5', + 'ELECTRIC YELLOW': '#FFFF33', + 'BUTTERMILK': '#F8E4A0', + 'LEMON CHIFFON': '#FFFACD', + + # Additional warm hues + 'VERMILION': '#E34234', + 'TANGERINE': '#F28500', + 'TERRACOTTA': '#E2725B', + 'BURGUNDY': '#800020', + 'MARS RED': '#CE1620', + + # Greens + 'LIME GREEN': '#32CD32', + 'VIRIDIAN': '#40826D', + 'PHTHALO GREEN': '#123524', + "HOOKER'S GREEN": '#49796B', + 'SAP GREEN': '#507D2A', + 'OLIVE GREEN': '#BAB86C', + 'CINNABAR GREEN': '#61B329', + 'EMERALD GREEN': '#50C878', + 'AQUA GREEN CHROMIUM': '#65B8A7', + 'MINT GREEN': '#98FF98', + 'FOREST GREEN': '#228B22', + 'CHROME GREEN': '#2E4A25', + 'CERULEAN GREEN': '#0F9B8E', + 'PISTACHIO': '#93C572', + 'SEAFOAM': '#98FF98', + 'HONEYDEW': '#F0FFF0', + 'YELLOW GREEN': '#9ACD32', + 'CELADON': '#ACE1AF', + 'VERDIGRIS': '#43B3AE', + 'AQUAMARINE': '#7FFFD4', + 'TURQUOISE': '#40E0D0', + + # Blues + 'ULTRAMARINE BLUE': '#1B1B7A', + 'COBALT BLUE': '#0047AB', + 'CERULEAN BLUE': '#2A52BE', + 'PHTHALO BLUE': '#000F89', + 'PRUSSIAN BLUE': '#003153', + 'ROYAL BLUE': '#4169E1', + 'SKY BLUE': '#87CEEB', + 'EGYPTIAN BLUE': '#1034A6', + 'AQUAMARINE': '#7FFFD4', + 'DENIM BLUE': '#1560BD', + 'NAVY': '#000080', + 'LAPIS LAZULI': '#26619C', + 'INDIGO': '#4B0082', + 'POWDER BLUE': '#B6D0E2', + 'BABY BLUE': '#89CFF0', + 'DUCK EGG BLUE': '#E0FFFF', + 'TURQUOISE': '#40E0D0', + 'AZURE': '#007FFF', + 'SAPPHIRE BLUE': '#0F52BA', + 'DODGER BLUE': '#1E90FF', + + # Purples + 'COBALT VIOLET LIGHT': '#CF71AF', + 'DIOXAZINE PURPLE': '#6C3082', + 'QUINACRIDONE VIOLET': '#B385BB', + 'LILAC': '#C8A2C8', + 'AMETHYST PURPLE': '#9966CC', + 'PLUM': '#8E4585', + 'ORCHID': '#DA70D6', + 'LAVENDER': '#E6E6FA', + 'PERIWINKLE': '#CCCCFF', + 'ULTRAMARINE VIOLET': '#5C246E', + 'MAUVE': '#E0B0FF', + 'FUCHSIA': '#FF00FF', + 'BLUE-VIOLET': '#8A2BE2', + + # Browns + 'BURNT SIENNA': '#E97451', + 'RAW SIENNA': '#D68A59', + 'TERRACOTTA': '#E2725B', + 'BURNT UMBER': '#8A3324', + 'TAUPE': '#483C32', + 'SEPIA': '#704214', + + # Greys and Neutrals + 'BEIGE': '#F5F5DC', + 'ALABASTER': '#FEFEE2', + 'WHITE': '#FFFFFF', + 'PEARL WHITE': '#FDEEF4', + "PAYNE'S GRAY": '#536878', + 'NEUTRAL GRAY': '#808080', + 'WARM GRAY': '#808077', + 'COOL GRAY': '#798081', + 'CHARCOAL GRAY': '#364135', + 'GRAPHITE GRAY': '#474A51', + 'STONE GRAY': '#928E85', + 'SILVER GRAY': '#C0C0C0', + 'DARK SLATE GRAY': '#2F4F4F', + 'IVORY BLACK': '#292421', + 'MARS BLACK': '#232323', + + # Neutrals (lighter tones) + 'CREAM': '#FFFDD0', + 'EGGSHELL': '#F0EAD6', + + # Peach/Coral tones + 'PEACH': '#FFE5B4', + 'CORAL': '#FF7F50', + 'APRICOT': '#FBCEB1', + + # Specialty/Metallic + 'GOLD': '#FFD700', + 'SILVER': '#C0C0C0', + 'COPPER': '#B87333', + 'BRONZE': '#CD7F32', + 'ROSE GOLD': '#B76E79', + 'ROSE BRONZE': '#B76E43', + 'GUNMETAL': '#2C3539', + 'PLATINUM': '#E5E4E2', + 'ANTIQUE GOLD': '#CFB53B', + + # Specialty Finishes + 'MINT CREAM': '#F5FFFA', + 'PEWTER': '#899499' +} + +# Color Families +color_families = { + 'Reds': [ + 'CRIMSON', 'CADMIUM RED', 'CADMIUM RED DEEP', 'PYRROLE RED', + 'SCARLET', 'CARMINE', 'MAGENTA', + 'VENETIAN RED', 'INDIAN RED', 'RUBY RED', + 'VERMILION', 'MARS RED', 'TRUE RED' + ], + 'Pinks': [ + 'POWDER PINK', 'STRAWBERRY PINK', 'HOT PINK', 'PERSIAN ROSE', + 'FLAMINGO PINK', 'ROSE BLUSH', 'BALLET PINK', 'BLUSH ROSE', + 'FUCHSIA', 'MAGENTA', 'ELECTRIC PINK', 'COTTON CANDY PINK', 'CORAL' + ], + 'Oranges': [ + 'CADMIUM ORANGE', 'BURNT ORANGE', 'PERSIMMON', 'SUNSET ORANGE', + 'APRICOT ORANGE', 'PASTEL ORANGE', 'BURNT SIENNA', 'RAW SIENNA', + 'TANGERINE', 'ORANGE CREAM', 'MANGO' + ], + 'Yellows': [ + 'GOLD OCHRE', 'YELLOW OCHRE', 'NAPLES YELLOW', 'INDIAN YELLOW', + 'AZO YELLOW', 'HANSA YELLOW', 'CANARY YELLOW', 'MARIGOLD', + 'AMBER', 'WARM YELLOW', 'LEMON YELLOW', 'PASTEL YELLOW', + 'ELECTRIC YELLOW', 'BUTTERMILK', 'LEMON CHIFFON' + ], + 'Greens': [ + 'LIME GREEN', 'VIRIDIAN', 'PHTHALO GREEN', "HOOKER'S GREEN", + 'SAP GREEN', 'OLIVE GREEN', 'CINNABAR GREEN', 'EMERALD GREEN', + 'AQUA GREEN CHROMIUM', 'MINT GREEN', 'FOREST GREEN', 'CHROME GREEN', + 'CERULEAN GREEN', 'PISTACHIO', 'SEAFOAM', 'HONEYDEW', + 'YELLOW GREEN', 'CELADON', 'VERDIGRIS', 'AQUAMARINE', 'TURQUOISE' + ], + 'Blues': [ + 'ULTRAMARINE BLUE', 'COBALT BLUE', 'CERULEAN BLUE', 'PHTHALO BLUE', + 'PRUSSIAN BLUE', 'ROYAL BLUE', 'SKY BLUE', 'EGYPTIAN BLUE', + 'AQUAMARINE', 'DENIM BLUE', 'NAVY', 'LAPIS LAZULI', 'INDIGO', + 'POWDER BLUE', 'BABY BLUE', 'DUCK EGG BLUE', + 'TURQUOISE', 'AZURE', 'SAPPHIRE BLUE', 'DODGER BLUE' + ], + 'Purples': [ + 'COBALT VIOLET LIGHT', 'DIOXAZINE PURPLE', 'QUINACRIDONE VIOLET', + 'LILAC', 'AMETHYST PURPLE', 'PLUM', 'ORCHID', 'LAVENDER', + 'PERIWINKLE', 'ULTRAMARINE VIOLET', 'MAUVE', 'FUCHSIA', + 'BLUE-VIOLET' + ], + 'Browns': [ + 'BURNT SIENNA', 'RAW SIENNA', 'TERRACOTTA', 'BURNT UMBER', + 'TAUPE', 'SEPIA' + ], + 'Greys': [ + "PAYNE'S GRAY", 'NEUTRAL GRAY', 'WARM GRAY', 'COOL GRAY', + 'CHARCOAL GRAY', 'GRAPHITE GRAY', 'STONE GRAY', 'SILVER GRAY', + 'DARK SLATE GRAY', 'GUNMETAL', 'PEWTER', + 'WHITE', 'ALABASTER', 'PEARL WHITE', + 'IVORY BLACK', 'MARS BLACK' + ], + 'Neutrals': [ + 'BEIGE', 'ALABASTER', 'WHITE', 'PEARL WHITE', + 'CREAM', 'EGGSHELL' + ], + 'Metallics': [ + 'GOLD', 'SILVER', 'COPPER', 'BRONZE', 'ROSE GOLD', 'ROSE BRONZE', + 'PLATINUM', 'ANTIQUE GOLD', 'GUNMETAL' + ], + 'Broad Neutrals': [], # Will be computed dynamically. + 'Pastels': [ + 'POWDER PINK', 'BALLET PINK', 'ROSE BLUSH', 'PASTEL ORANGE', + 'POWDER BLUE', 'BABY BLUE', 'DUCK EGG BLUE', + 'HONEYDEW', 'MINT CREAM', 'LILAC', 'MAUVE', 'ORANGE CREAM', + 'LEMON CHIFFON', 'CELADON', 'PASTEL YELLOW', + ], + 'Peaches': [ + 'PEACH', 'CORAL', 'APRICOT', 'APRICOT ORANGE' + ], + 'Warm Colors': [ + 'CRIMSON', 'CADMIUM RED', 'CADMIUM RED DEEP', 'PYRROLE RED', + 'SCARLET', 'CARMINE', 'MAGENTA', + 'VENETIAN RED', 'INDIAN RED', 'RUBY RED', + 'VERMILION', 'MARS RED', 'TRUE RED', + 'POWDER PINK', 'STRAWBERRY PINK', 'HOT PINK', 'PERSIAN ROSE', + 'FLAMINGO PINK', 'ROSE BLUSH', 'BALLET PINK', 'BLUSH ROSE', + 'ELECTRIC PINK', 'COTTON CANDY PINK', + 'CADMIUM ORANGE', 'BURNT ORANGE', 'PERSIMMON', 'SUNSET ORANGE', + 'APRICOT ORANGE', 'PASTEL ORANGE', 'BURNT SIENNA', 'RAW SIENNA', 'TANGERINE', + 'ORANGE CREAM', 'MANGO', + 'GOLD OCHRE', 'YELLOW OCHRE', 'NAPLES YELLOW', 'INDIAN YELLOW', + 'AZO YELLOW', 'HANSA YELLOW', 'CANARY YELLOW', 'MARIGOLD', + 'AMBER', 'WARM YELLOW', 'LEMON YELLOW', 'PASTEL YELLOW', + 'TERRACOTTA', 'BURNT UMBER', 'PEACH', 'CORAL', 'APRICOT', + 'GOLD', 'ANTIQUE GOLD' + ], + 'Cool Colors': [ + 'LIME GREEN', 'VIRIDIAN', 'PHTHALO GREEN', "HOOKER'S GREEN", + 'SAP GREEN', 'OLIVE GREEN', 'CINNABAR GREEN', 'EMERALD GREEN', + 'AQUA GREEN CHROMIUM', 'MINT GREEN', 'FOREST GREEN', 'CHROME GREEN', + 'CERULEAN GREEN', 'PISTACHIO', 'SEAFOAM', 'HONEYDEW', + 'ULTRAMARINE BLUE', 'ROYAL BLUE', 'SKY BLUE', 'EGYPTIAN BLUE', 'AQUAMARINE', + 'DENIM BLUE', 'NAVY', 'LAPIS LAZULI', 'INDIGO', + 'POWDER BLUE', 'BABY BLUE', 'DUCK EGG BLUE', 'TURQUOISE', + 'COBALT VIOLET LIGHT', 'DIOXAZINE PURPLE', 'QUINACRIDONE VIOLET', + 'LILAC', 'AMETHYST PURPLE', 'PLUM', 'ORCHID', 'LAVENDER', + 'PERIWINKLE', 'ULTRAMARINE VIOLET', 'MAUVE', + 'AZURE', 'DODGER BLUE', 'CERULEAN BLUE', 'COBALT BLUE', 'PRUSSIAN BLUE' + ] +} + +# Dynamically compute Broad Neutrals as the union of Neutrals, Greys, Browns, and Metallics. +broad_neutrals = set(color_families['Neutrals'] + color_families['Greys'] + color_families['Browns'] + color_families['Metallics']) +color_families['Broad Neutrals'] = sorted(list(broad_neutrals)) + +# Each palette is defined as one list: +# The first element is the palette name (a descriptive title), +# and the remaining elements are the color names. +# All categories are stored as one large list of palettes. +COLOR_PALETTES = { + # each complementary entry should be two complementary colors and a 'broad neutral' color + 'complementary': [ + ], + + # 4 similar or adjacent colors + 'analogous': [ + ], + + #A Triadic color scheme uses three colors that are evenly spaced 120 degrees apart on the color wheel. This creates a balanced, vibrant, and high-contrast palette while maintaining harmony. + 'triadic': [ + ], + + # A Split Complementary color scheme consists of a base color and the two colors adjacent to its direct complement on the color wheel. This provides strong contrast like a complementary scheme but with less tension and more harmony. + 'split_complementary': [ + ], + + # A Tetradic color scheme uses two complementary color pairs (four colors total), forming a rectangle on the color wheel. This creates a vibrant, diverse palette but requires balance to avoid clashing + 'tetradic': [ + ], + + # A Rectangular Tetradic color scheme is a variation of Tetradic, where the four colors are spaced unevenly around the color wheel, forming a rectangle. It typically includes one dominant color and three supporting colors, offering rich contrast and versatility while requiring careful balance. + 'rectangular_tetradic': [ + ] +} diff --git a/flux_example_ebu_prompthelper.json b/flux_example_ebu_prompthelper.json new file mode 100644 index 0000000..18158ed --- /dev/null +++ b/flux_example_ebu_prompthelper.json @@ -0,0 +1,2562 @@ +{ + "last_node_id": 190, + "last_link_id": 426, + "nodes": [ + { + "id": 25, + "type": "RandomNoise", + "pos": [ + 4064, + -104 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "NOISE", + "type": "NOISE", + "links": [ + 37 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "RandomNoise" + }, + "widgets_values": [ + 270912215733178, + "randomize" + ] + }, + { + "id": 10, + "type": "VAELoader", + "pos": [ + 4456, + -64 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 12 + ], + "slot_index": 0, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "VAELoader" + }, + "widgets_values": [ + "ae.sft" + ] + }, + { + "id": 17, + "type": "BasicScheduler", + "pos": [ + 4056, + 152 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 38, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 20 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "BasicScheduler" + }, + "widgets_values": [ + "simple", + 8, + 1 + ] + }, + { + "id": 22, + "type": "BasicGuider", + "pos": [ + 4040, + -712 + ], + "size": [ + 241.79998779296875, + 46 + ], + "flags": {}, + "order": 46, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 39, + "slot_index": 0 + }, + { + "name": "conditioning", + "type": "CONDITIONING", + "link": 61, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 30 + ], + "slot_index": 0, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "BasicGuider" + }, + "widgets_values": [] + }, + { + "id": 16, + "type": "KSamplerSelect", + "pos": [ + 4064, + 40 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 19 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "KSamplerSelect" + }, + "widgets_values": [ + "dpmpp_2m" + ] + }, + { + "id": 12, + "type": "UNETLoader", + "pos": [ + 3528, + -600 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 38, + 39 + ], + "slot_index": 0, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "UNETLoader" + }, + "widgets_values": [ + "flux1-schnell-fp8.safetensors", + "default" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 11, + "type": "DualCLIPLoader", + "pos": [ + 3696, + -224 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 10 + ], + "slot_index": 0, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "DualCLIPLoader" + }, + "widgets_values": [ + "clip_l.safetensors", + "t5xxl_fp16.safetensors", + "flux", + "default" + ] + }, + { + "id": 113, + "type": "ShowText|pysssss", + "pos": [ + 1937.2314453125, + -506.3834228515625 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 416, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "triadic" + ] + }, + { + "id": 116, + "type": "ShowText|pysssss", + "pos": [ + 1591.0291748046875, + -203.83721923828125 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 413, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "cinnabar green" + ] + }, + { + "id": 115, + "type": "ShowText|pysssss", + "pos": [ + 1591.022216796875, + -314.81500244140625 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 412, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "indian red" + ] + }, + { + "id": 117, + "type": "ShowText|pysssss", + "pos": [ + 1589.6849365234375, + -85.42388153076172 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 414, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 118, + "type": "ShowText|pysssss", + "pos": [ + 1599.1583251953125, + 43.82836151123047 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 415, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 119, + "type": "ShowText|pysssss", + "pos": [ + 2024.5435791015625, + -219.0660858154297 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 417, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "#4169E1, #CD5C5C, #61B329" + ] + }, + { + "id": 114, + "type": "ShowText|pysssss", + "pos": [ + 1584.0858154296875, + -431.30438232421875 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 411, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "royal blue" + ] + }, + { + "id": 136, + "type": "String-🔬", + "pos": [ + 2433.74267578125, + -1438.6474609375 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "We consume the 3 list elements randomly one at a time to ensure that they are all used and there are no duplicates." + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 133, + "type": "String-🔬", + "pos": [ + 1972.30859375, + -1130.05712890625 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "replacing commas with newlines to turn this into a list" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 144, + "type": "String-🔬", + "pos": [ + 1044.3729248046875, + -765.3875732421875 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "Generating our color palette. This custom node is a beast with many options. Experiment and find settings you like." + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 3920, + -536 + ], + "size": [ + 422.84503173828125, + 164.31304931640625 + ], + "flags": {}, + "order": 44, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 10 + }, + { + "name": "text", + "type": "STRING", + "link": 348, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 60 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "an oil painting of a duck" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + 4064, + -264 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 23 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 640, + 480, + 1 + ] + }, + { + "id": 165, + "type": "EbuPromptHelperRandomize", + "pos": [ + 2439.874267578125, + -492.8241271972656 + ], + "size": [ + 400, + 218 + ], + "flags": {}, + "order": 37, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 375, + "widget": { + "name": "prompt_text" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 377 + ] + }, + { + "name": "word_selected", + "type": "STRING", + "links": null + } + ], + "title": "EbuPromptHelperRandomize", + "properties": { + "Node name for S&R": "EbuPromptHelperRandomize" + }, + "widgets_values": [ + "dog; cat; lion; tiger; lizard", + "MY_ANIMAL", + "dog; cat; lion; tiger; lizard", + 1039, + "randomize", + true, + "semi-colons" + ] + }, + { + "id": 162, + "type": "EbuPromptHelperConsumeListItem", + "pos": [ + 3171.906494140625, + -1197.29052734375 + ], + "size": [ + 321.5240173339844, + 255.1984405517578 + ], + "flags": {}, + "order": 36, + "mode": 0, + "inputs": [ + { + "name": "list", + "type": "STRING", + "link": 403, + "widget": { + "name": "list" + } + }, + { + "name": "prompt_text", + "type": "STRING", + "link": 402, + "widget": { + "name": "prompt_text" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 375 + ] + }, + { + "name": "word_selected", + "type": "STRING", + "links": null + }, + { + "name": "revised_list", + "type": "STRING", + "links": null + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperConsumeListItem" + }, + "widgets_values": [ + "MY_COLOR3", + "", + 23799047713127, + "randomize", + "" + ] + }, + { + "id": 143, + "type": "String-🔬", + "pos": [ + 2434.68359375, + -758.7741088867188 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "Here we're replacing variables with random options. Note how you have some flexibiliy with the format of your options using delimit_options_with" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 169, + "type": "EbuPromptHelperReplace", + "pos": [ + 2615.122314453125, + 1218.2530517578125 + ], + "size": [ + 286.34625244140625, + 174.828369140625 + ], + "flags": {}, + "order": 33, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 397, + "widget": { + "name": "prompt_text" + } + }, + { + "name": "replace_with", + "type": "STRING", + "link": 383, + "widget": { + "name": "replace_with" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 382 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperReplace" + }, + "widgets_values": [ + "", + "MY_NOW", + "", + true + ] + }, + { + "id": 148, + "type": "EbuPromptHelperReplace", + "pos": [ + 2889.15771484375, + 529.489013671875 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 40, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 337, + "widget": { + "name": "prompt_text" + } + }, + { + "name": "replace_with", + "type": "STRING", + "link": 426, + "widget": { + "name": "replace_with" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 340 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperReplace" + }, + "widgets_values": [ + "", + "MY_WEATHER", + "", + true + ] + }, + { + "id": 147, + "type": "EbuPromptHelperReplace", + "pos": [ + 2882.795654296875, + 128.46385192871094 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 39, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 378, + "widget": { + "name": "prompt_text" + } + }, + { + "name": "replace_with", + "type": "STRING", + "link": 423, + "widget": { + "name": "replace_with" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 337 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperReplace" + }, + "widgets_values": [ + "", + "MY_WHEN", + "", + true + ] + }, + { + "id": 173, + "type": "ShowText|pysssss", + "pos": [ + 2915.683349609375, + 388.51513671875 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 30, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 425, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "late afternoon during early fall" + ] + }, + { + "id": 129, + "type": "ShowText|pysssss", + "pos": [ + 2405.075439453125, + 924.076904296875 + ], + "size": [ + 506.8660583496094, + 166.4232940673828 + ], + "flags": {}, + "order": 41, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 340, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 345 + ], + "slot_index": 0, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "A children's book illustration of dog with a cinnabar green hat and royal blue suit and indian red tie at neighborhood block party. The scene takes place late afternoon during early fall of 1991 and this is the weather: clear with a gentle scattering of clouds modestly sustaining a vivid, dry cool environment." + ] + }, + { + "id": 179, + "type": "EbuPromptHelperReplace", + "pos": [ + 2128.163818359375, + 1203.18310546875 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 31, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 396, + "widget": { + "name": "prompt_text" + } + }, + { + "name": "replace_with", + "type": "STRING", + "link": 399, + "widget": { + "name": "replace_with" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 397 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperReplace" + }, + "widgets_values": [ + "", + "[QUALITIES]", + "", + true + ] + }, + { + "id": 151, + "type": "String-🔬", + "pos": [ + 1543.0611572265625, + 1203.37451171875 + ], + "size": [ + 502.5408630371094, + 176.64910888671875 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 396 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "The artstyle is whisical and a mixture of illustration and painting.\n\nThis image should have the following qualities:\n[QUALITIES]\n\nGenerated on MY_NOW" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 180, + "type": "String-🔬", + "pos": [ + 1495.9063720703125, + 1530.295654296875 + ], + "size": [ + 246.2478485107422, + 178.17466735839844 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 398 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "cute\nfunny\nheartfelt\nrural\nexciting\nhappy\nfun" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 181, + "type": "EbuPromptHelperListSampler", + "pos": [ + 1820.9208984375, + 1508.5657958984375 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "list", + "type": "STRING", + "link": 398, + "widget": { + "name": "list" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 399 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperListSampler" + }, + "widgets_values": [ + "", + 878297896035717, + "randomize", + 3, + true + ] + }, + { + "id": 170, + "type": "EbuPromptHelperCurrentDateTime", + "pos": [ + 2544.280517578125, + 1501.45751953125 + ], + "size": [ + 415.8000183105469, + 86 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "date", + "type": "STRING", + "links": null, + "slot_index": 0 + }, + { + "name": "time", + "type": "STRING", + "links": null + }, + { + "name": "datetime", + "type": "STRING", + "links": [ + 383 + ], + "slot_index": 2 + }, + { + "name": "datetime_for_filename", + "type": "STRING", + "links": null + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperCurrentDateTime" + }, + "widgets_values": [] + }, + { + "id": 166, + "type": "EbuPromptHelperRandomize", + "pos": [ + 2881.716796875, + -510.44976806640625 + ], + "size": [ + 409.6338806152344, + 286.8024597167969 + ], + "flags": {}, + "order": 38, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 377, + "widget": { + "name": "prompt_text" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 378 + ] + }, + { + "name": "word_selected", + "type": "STRING", + "links": null + } + ], + "title": "EbuPromptHelperRandomize", + "properties": { + "Node name for S&R": "EbuPromptHelperRandomize" + }, + "widgets_values": [ + "", + "MY_LOCATION", + "on main street in a small town\nin outdoor patio seeting at a fancy restaurant\nat a farmer's market\ncollege campus\nneighborhood block party", + 17, + "randomize", + true, + "newlines" + ] + }, + { + "id": 168, + "type": "String-🔬", + "pos": [ + 2417.8330078125, + -29.000301361083984 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "Now we're generating random season, time of day, year, and weather" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 182, + "type": "String-🔬", + "pos": [ + 1071.500244140625, + 1515.675537109375 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "Here's the List Samper at work!" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 183, + "type": "String-🔬", + "pos": [ + 3010.14990234375, + 846.6455688476562 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "Note how we can merge strings" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 154, + "type": "EbuPromptHelperCombineTwoStrings", + "pos": [ + 2997.95751953125, + 1115.8934326171875 + ], + "size": [ + 453.5999755859375, + 200 + ], + "flags": {}, + "order": 42, + "mode": 0, + "inputs": [ + { + "name": "str1", + "type": "STRING", + "link": 345, + "widget": { + "name": "str1" + } + }, + { + "name": "str2", + "type": "STRING", + "link": 382, + "widget": { + "name": "str2" + } + } + ], + "outputs": [ + { + "name": "combined", + "type": "STRING", + "links": [ + 394 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperCombineTwoStrings" + }, + "widgets_values": [ + "", + "", + "\n\n" + ] + }, + { + "id": 184, + "type": "EbuPromptHelperConsumeListItem", + "pos": [ + 2795.91064453125, + -1186.3201904296875 + ], + "size": [ + 321.5240173339844, + 255.1984405517578 + ], + "flags": {}, + "order": 35, + "mode": 0, + "inputs": [ + { + "name": "list", + "type": "STRING", + "link": 407, + "widget": { + "name": "list" + } + }, + { + "name": "prompt_text", + "type": "STRING", + "link": 406, + "widget": { + "name": "prompt_text" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 402 + ] + }, + { + "name": "word_selected", + "type": "STRING", + "links": null + }, + { + "name": "revised_list", + "type": "STRING", + "links": [ + 403 + ] + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperConsumeListItem" + }, + "widgets_values": [ + "MY_COLOR2", + "", + 264975990745409, + "randomize", + "" + ] + }, + { + "id": 185, + "type": "EbuPromptHelperConsumeListItem", + "pos": [ + 2433.427978515625, + -1181.4920654296875 + ], + "size": [ + 321.5240173339844, + 255.1984405517578 + ], + "flags": {}, + "order": 34, + "mode": 0, + "inputs": [ + { + "name": "list", + "type": "STRING", + "link": 409, + "widget": { + "name": "list" + } + }, + { + "name": "prompt_text", + "type": "STRING", + "link": 405, + "widget": { + "name": "prompt_text" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 406 + ] + }, + { + "name": "word_selected", + "type": "STRING", + "links": null + }, + { + "name": "revised_list", + "type": "STRING", + "links": [ + 407 + ] + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperConsumeListItem" + }, + "widgets_values": [ + "MY_COLOR1", + "", + 325300617429020, + "randomize", + "" + ] + }, + { + "id": 112, + "type": "ShowText|pysssss", + "pos": [ + 1579.8402099609375, + -551.3126220703125 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 410, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 408 + ], + "slot_index": 0, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "royal blue, indian red, cinnabar green" + ] + }, + { + "id": 186, + "type": "EbuPromptHelperReplace", + "pos": [ + 1969.9266357421875, + -869.6386108398438 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 32, + "mode": 0, + "inputs": [ + { + "name": "prompt_text", + "type": "STRING", + "link": 408, + "widget": { + "name": "prompt_text" + } + } + ], + "outputs": [ + { + "name": "updated_prompt_text", + "type": "STRING", + "links": [ + 409 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperReplace" + }, + "widgets_values": [ + "", + ",", + "\n", + true + ] + }, + { + "id": 187, + "type": "EbuPromptHelperRandomColorPalette", + "pos": [ + 1055.2052001953125, + -474.19586181640625 + ], + "size": [ + 390.5999755859375, + 486 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "palette_string", + "type": "STRING", + "links": [ + 410 + ] + }, + { + "name": "color1", + "type": "STRING", + "links": [ + 411 + ] + }, + { + "name": "color2", + "type": "STRING", + "links": [ + 412 + ] + }, + { + "name": "color3", + "type": "STRING", + "links": [ + 413 + ] + }, + { + "name": "color4", + "type": "STRING", + "links": [ + 414 + ] + }, + { + "name": "color5", + "type": "STRING", + "links": [ + 415 + ] + }, + { + "name": "palette_type", + "type": "STRING", + "links": [ + 416 + ] + }, + { + "name": "hex_values", + "type": "STRING", + "links": [ + 417 + ] + } + ], + "title": "EBU Prompt Helper Color Palette", + "properties": { + "Node name for S&R": "EbuPromptHelperRandomColorPalette" + }, + "widgets_values": [ + false, + false, + false, + false, + false, + false, + false, + true, + "3 colors", + "None", + "Greys", + 943778883629685, + "randomize" + ] + }, + { + "id": 127, + "type": "String-🔬", + "pos": [ + 1796.367431640625, + -1589.380126953125 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 405 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String-🔬" + }, + "widgets_values": [ + "A children's book illustration of MY_ANIMAL with a MY_COLOR1 hat and MY_COLOR2 suit and MY_COLOR3 tie at MY_LOCATION. The scene takes place MY_WHEN and this is the weather: MY_WEATHER." + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 188, + "type": "ShowText|pysssss", + "pos": [ + 3265.5693359375, + 382.085205078125 + ], + "size": [ + 369.8238220214844, + 76 + ], + "flags": {}, + "order": 29, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 424, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "late afternoon during early fall of 1991" + ] + }, + { + "id": 26, + "type": "PreviewImage", + "pos": [ + 4129.19580078125, + 903.8606567382812 + ], + "size": [ + 528.840576171875, + 480.5522155761719 + ], + "flags": {}, + "order": 49, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 41 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 4441.455078125, + 662.7484741210938 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 48, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 24 + }, + { + "name": "vae", + "type": "VAE", + "link": 12 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 41 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 13, + "type": "SamplerCustomAdvanced", + "pos": [ + 3976.080322265625, + 507.62774658203125 + ], + "size": [ + 355.20001220703125, + 326 + ], + "flags": {}, + "order": 47, + "mode": 0, + "inputs": [ + { + "name": "noise", + "type": "NOISE", + "link": 37, + "slot_index": 0 + }, + { + "name": "guider", + "type": "GUIDER", + "link": 30, + "slot_index": 1 + }, + { + "name": "sampler", + "type": "SAMPLER", + "link": 19, + "slot_index": 2 + }, + { + "name": "sigmas", + "type": "SIGMAS", + "link": 20, + "slot_index": 3 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 23, + "slot_index": 4 + } + ], + "outputs": [ + { + "name": "output", + "type": "LATENT", + "links": [ + 24 + ], + "slot_index": 0, + "shape": 3 + }, + { + "name": "denoised_output", + "type": "LATENT", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "SamplerCustomAdvanced" + }, + "widgets_values": [], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 43, + "type": "FluxGuidance", + "pos": [ + 4008, + -880 + ], + "size": [ + 320, + 58 + ], + "flags": {}, + "order": 45, + "mode": 0, + "inputs": [ + { + "name": "conditioning", + "type": "CONDITIONING", + "link": 60 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 61 + ], + "slot_index": 0, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "FluxGuidance" + }, + "widgets_values": [ + 21.1 + ] + }, + { + "id": 190, + "type": "EbuPromptHelperSeasonWeatherTimeOfDay", + "pos": [ + 2158.384521484375, + 246.84896850585938 + ], + "size": [ + 648.3599853515625, + 300.0320739746094 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "when", + "type": "STRING", + "links": [ + 423, + 424 + ] + }, + { + "name": "when_no_year", + "type": "STRING", + "links": [ + 425 + ] + }, + { + "name": "weather", + "type": "STRING", + "links": [ + 426 + ] + } + ], + "properties": { + "Node name for S&R": "EbuPromptHelperSeasonWeatherTimeOfDay" + }, + "widgets_values": [ + 1980, + 2000, + "6:00am", + "7:00pm", + 24090015489304, + "randomize", + "no skew", + "no skew" + ] + }, + { + "id": 155, + "type": "ShowText|pysssss", + "pos": [ + 3531.598876953125, + 1083.313720703125 + ], + "size": [ + 466.27313232421875, + 240.74607849121094 + ], + "flags": {}, + "order": 43, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 394, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 348 + ], + "slot_index": 0, + "shape": 6 + } + ], + "title": "Final Prompt", + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "A children's book illustration of dog with a cinnabar green hat and royal blue suit and indian red tie at neighborhood block party. The scene takes place late afternoon during early fall of 1991 and this is the weather: clear with a gentle scattering of clouds modestly sustaining a vivid, dry cool environment.\n\nThe artstyle is whisical and a mixture of illustration and painting.\n\nThis image should have the following qualities:\n1. cute\n2. fun\n3. funny\n\nGenerated on 2025-02-10 9:55:27pm" + ], + "color": "#232", + "bgcolor": "#353" + } + ], + "links": [ + [ + 10, + 11, + 0, + 6, + 0, + "CLIP" + ], + [ + 12, + 10, + 0, + 8, + 1, + "VAE" + ], + [ + 19, + 16, + 0, + 13, + 2, + "SAMPLER" + ], + [ + 20, + 17, + 0, + 13, + 3, + "SIGMAS" + ], + [ + 23, + 5, + 0, + 13, + 4, + "LATENT" + ], + [ + 24, + 13, + 0, + 8, + 0, + "LATENT" + ], + [ + 30, + 22, + 0, + 13, + 1, + "GUIDER" + ], + [ + 37, + 25, + 0, + 13, + 0, + "NOISE" + ], + [ + 38, + 12, + 0, + 17, + 0, + "MODEL" + ], + [ + 39, + 12, + 0, + 22, + 0, + "MODEL" + ], + [ + 41, + 8, + 0, + 26, + 0, + "IMAGE" + ], + [ + 60, + 6, + 0, + 43, + 0, + "CONDITIONING" + ], + [ + 61, + 43, + 0, + 22, + 1, + "CONDITIONING" + ], + [ + 337, + 147, + 0, + 148, + 0, + "STRING" + ], + [ + 340, + 148, + 0, + 129, + 0, + "STRING" + ], + [ + 345, + 129, + 0, + 154, + 0, + "STRING" + ], + [ + 348, + 155, + 0, + 6, + 1, + "STRING" + ], + [ + 375, + 162, + 0, + 165, + 0, + "STRING" + ], + [ + 377, + 165, + 0, + 166, + 0, + "STRING" + ], + [ + 378, + 166, + 0, + 147, + 0, + "STRING" + ], + [ + 382, + 169, + 0, + 154, + 1, + "STRING" + ], + [ + 383, + 170, + 2, + 169, + 1, + "STRING" + ], + [ + 394, + 154, + 0, + 155, + 0, + "STRING" + ], + [ + 396, + 151, + 0, + 179, + 0, + "STRING" + ], + [ + 397, + 179, + 0, + 169, + 0, + "STRING" + ], + [ + 398, + 180, + 0, + 181, + 0, + "STRING" + ], + [ + 399, + 181, + 0, + 179, + 1, + "STRING" + ], + [ + 402, + 184, + 0, + 162, + 1, + "STRING" + ], + [ + 403, + 184, + 2, + 162, + 0, + "STRING" + ], + [ + 405, + 127, + 0, + 185, + 1, + "STRING" + ], + [ + 406, + 185, + 0, + 184, + 1, + "STRING" + ], + [ + 407, + 185, + 2, + 184, + 0, + "STRING" + ], + [ + 408, + 112, + 0, + 186, + 0, + "STRING" + ], + [ + 409, + 186, + 0, + 185, + 0, + "STRING" + ], + [ + 410, + 187, + 0, + 112, + 0, + "STRING" + ], + [ + 411, + 187, + 1, + 114, + 0, + "STRING" + ], + [ + 412, + 187, + 2, + 115, + 0, + "STRING" + ], + [ + 413, + 187, + 3, + 116, + 0, + "STRING" + ], + [ + 414, + 187, + 4, + 117, + 0, + "STRING" + ], + [ + 415, + 187, + 5, + 118, + 0, + "STRING" + ], + [ + 416, + 187, + 6, + 113, + 0, + "STRING" + ], + [ + 417, + 187, + 7, + 119, + 0, + "STRING" + ], + [ + 423, + 190, + 0, + 147, + 1, + "STRING" + ], + [ + 424, + 190, + 0, + 188, + 0, + "STRING" + ], + [ + 425, + 190, + 1, + 173, + 0, + "STRING" + ], + [ + 426, + 190, + 2, + 148, + 1, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.672749994932599, + "offset": [ + -1320.2387530284686, + 204.0966866871853 + ] + }, + "node_versions": { + "comfy-core": "0.3.12", + "ComfyUI-Custom-Scripts": "bc8922deff73f59311c05cef27b9d4caaf43e87b", + "ComfyUI-Logic": "42d4f3df45fb7f0dd6e2201a14c07d4dd09f235d" + }, + "ue_links": [] + }, + "version": 0.4 +} \ No newline at end of file diff --git a/make_palette_analogous.py b/make_palette_analogous.py new file mode 100644 index 0000000..77228e7 --- /dev/null +++ b/make_palette_analogous.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +import sys +import random +import colorsys +import math +from .color_data import COLORS, color_families + +# ---------------------------- +# Conversion & Utility Functions +# ---------------------------- +def hex_to_rgb(hex_code): + hex_code = hex_code.lstrip('#') + r = int(hex_code[0:2], 16) / 255.0 + g = int(hex_code[2:4], 16) / 255.0 + b = int(hex_code[4:6], 16) / 255.0 + return (r, g, b) + +def rgb_to_hex(rgb): + return '#{:02x}{:02x}{:02x}'.format( + int(rgb[0] * 255), + int(rgb[1] * 255), + int(rgb[2] * 255) + ) + +def hex_to_rgb_tuple(hex_code): + hex_code = hex_code.lstrip('#') + return (int(hex_code[0:2], 16), + int(hex_code[2:4], 16), + int(hex_code[4:6], 16)) + +def color_distance(rgb1, rgb2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2))) + +def find_closest_color_name(target_hex, color_dict): + target_rgb = hex_to_rgb_tuple(target_hex) + closest_name = None + min_dist = float('inf') + for name, hex_code in color_dict.items(): + current_rgb = hex_to_rgb_tuple(hex_code) + dist = color_distance(target_rgb, current_rgb) + if dist < min_dist: + min_dist = dist + closest_name = name + return closest_name + +def shift_hue(hex_code, degree_offset): + r, g, b = hex_to_rgb(hex_code) + H, L, S = colorsys.rgb_to_hls(r, g, b) + offset = degree_offset / 360.0 + new_H = (H + offset) % 1.0 + new_rgb = colorsys.hls_to_rgb(new_H, L, S) + return rgb_to_hex(new_rgb) + +# ---------------------------- +# Analogous Palette Functions +# ---------------------------- +def analogous_palette(base_hex, offset_degrees=15): + """Returns three analogous colors: base, base+offset, and base-offset.""" + return [base_hex, + shift_hue(base_hex, offset_degrees), + shift_hue(base_hex, -offset_degrees)] + +def generate_random_base_color(): + families = ['Reds', 'Pinks', 'Oranges', 'Yellows', 'Greens', 'Blues', 'Purples'] + candidate_colors = [] + for fam in families: + candidate_colors.extend(color_families.get(fam, [])) + random_color_name = random.choice(candidate_colors) + return random_color_name, COLORS[random_color_name] + +# ---------------------------- +# Palette Generation: 4 vs. 5 Colors +# ---------------------------- +def generate_analogous_palette_4(): + """4-color version: 3 analogous colors + 1 Broad Neutral.""" + broad_neutrals = color_families.get("Broad Neutrals", []) + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + analog_hexes = analogous_palette(base_hex, offset_degrees=15) + analog_names = [find_closest_color_name(h, COLORS) for h in analog_hexes] + fourth = random.choice(broad_neutrals) + palette = analog_names + [fourth] + if len(set(palette)) == 4: + return palette + + # Fallback: If we can't get 4 unique colors after max attempts, + # try with a larger offset + _, base_hex = generate_random_base_color() + analog_hexes = analogous_palette(base_hex, offset_degrees=30) + analog_names = [find_closest_color_name(h, COLORS) for h in analog_hexes] + + # Try to find a unique neutral color + for neutral in broad_neutrals: + test_palette = analog_names + [neutral] + if len(set(test_palette)) == 4: + return test_palette + + # Ultimate fallback - use any unique color as the fourth + all_colors = list(COLORS.keys()) + for color in all_colors: + if color not in analog_names: + return analog_names + [color] + + # If everything fails, duplicate the last color + return analog_names + [analog_names[-1]] + +def generate_analogous_palette_5(): + """ + 5-color version: 3 original analogous colors + + one extra analogous variant (e.g. base+30°) + + one Broad Neutral. + """ + broad_neutrals = color_families.get("Broad Neutrals", []) + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + # Original analogous (3 colors) + analog_hexes = analogous_palette(base_hex, offset_degrees=15) + analog_names = [find_closest_color_name(h, COLORS) for h in analog_hexes] + # Extra analogous: base shifted by +30° + extra = find_closest_color_name(shift_hue(base_hex, 30), COLORS) + fourth = random.choice(broad_neutrals) + palette = analog_names + [extra, fourth] + if len(set(palette)) == 5: + return palette + + # Fallback: Try with different offset values + for offset in [20, 25, 30]: + _, base_hex = generate_random_base_color() + analog_hexes = analogous_palette(base_hex, offset_degrees=offset) + analog_names = [find_closest_color_name(h, COLORS) for h in analog_hexes] + extra = find_closest_color_name(shift_hue(base_hex, offset*2), COLORS) + + # Try each neutral color + current_palette = analog_names + [extra] + if len(set(current_palette)) == 4: # Make sure first 4 are unique + for neutral in broad_neutrals: + test_palette = current_palette + [neutral] + if len(set(test_palette)) == 5: + return test_palette + + # Ultimate fallback - start with analogous colors and add unique colors + _, base_hex = generate_random_base_color() + analog_hexes = analogous_palette(base_hex, offset_degrees=30) + current_palette = [find_closest_color_name(h, COLORS) for h in analog_hexes] + + # Try to add two more unique colors + all_colors = list(COLORS.keys()) + while len(current_palette) < 5 and all_colors: + candidate = random.choice(all_colors) + if candidate not in current_palette: + current_palette.append(candidate) + all_colors.remove(candidate) + + # If we still don't have 5 colors, pad with duplicates + while len(current_palette) < 5: + current_palette.append(current_palette[-1]) + + return current_palette + +if __name__ == '__main__': + five_flag = '-5' in sys.argv + if five_flag: + palette = generate_analogous_palette_5() + else: + palette = generate_analogous_palette_4() + print("Analogous Palette with Broad Neutral Twist (Color Names):", palette) \ No newline at end of file diff --git a/make_palette_art_house.py b/make_palette_art_house.py new file mode 100644 index 0000000..cb3580a --- /dev/null +++ b/make_palette_art_house.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import sys +import random +from .color_data import COLORS, color_families + +def generate_art_house_palette_4(): + greys = color_families.get("Greys", []) + if len(greys) < 3: + raise ValueError("Not enough grey colors available.") + + warm_colors = set(color_families.get("Warm Colors", [])) + excluded = set(color_families.get("Metallics", [])) | set(color_families.get("Greys", [])) | set(color_families.get("Pastels", [])) + valid_warm = list(warm_colors - excluded) + if not valid_warm: + raise ValueError("No valid Warm Colors available.") + + max_attempts = 50 + for _ in range(max_attempts): + grey_palette = random.sample(greys, 3) + warm_color = random.choice(valid_warm) + palette = grey_palette + [warm_color] + if len(set(palette)) == 4: + return palette + + # Fallback: If we couldn't get 4 unique colors after max attempts, + # try different combinations of greys and warm colors + grey_palette = random.sample(greys, 3) + for warm_color in valid_warm: + palette = grey_palette + [warm_color] + if len(set(palette)) == 4: + return palette + + # Ultimate fallback - use any 3 unique greys and duplicate the last one + grey_palette = random.sample(greys, 3) + return grey_palette + [grey_palette[-1]] + +def generate_art_house_palette_5(): + greys = color_families.get("Greys", []) + if len(greys) < 4: + raise ValueError("Not enough grey colors available for 5-color palette.") + + warm_colors = set(color_families.get("Warm Colors", [])) + excluded = set(color_families.get("Metallics", [])) | set(color_families.get("Greys", [])) | set(color_families.get("Pastels", [])) + valid_warm = list(warm_colors - excluded) + if not valid_warm: + raise ValueError("No valid Warm Colors available.") + + max_attempts = 50 + for _ in range(max_attempts): + grey_palette = random.sample(greys, 4) + warm_color = random.choice(valid_warm) + palette = grey_palette + [warm_color] + if len(set(palette)) == 5: + return palette + + # Fallback: If we couldn't get 5 unique colors after max attempts, + # try different combinations of greys and warm colors + grey_palette = random.sample(greys, 4) + for warm_color in valid_warm: + palette = grey_palette + [warm_color] + if len(set(palette)) == 5: + return palette + + # Ultimate fallback - use any 4 unique greys and duplicate the last one + grey_palette = random.sample(greys, 4) + return grey_palette + [grey_palette[-1]] + +if __name__ == '__main__': + use_five = '-5' in sys.argv + if use_five: + palette = generate_art_house_palette_5() + else: + palette = generate_art_house_palette_4() + print("Art-House Palette (Unique Color Names):", palette) diff --git a/make_palette_chaotic.py b/make_palette_chaotic.py new file mode 100644 index 0000000..cb440be --- /dev/null +++ b/make_palette_chaotic.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys +import random +from .color_data import COLORS + +def generate_chaotic_palette(num_colors=4): + all_colors = list(COLORS.keys()) + return random.sample(all_colors, num_colors) + +if __name__ == '__main__': + use_five = '-5' in sys.argv + num = 5 if use_five else 4 + palette = generate_chaotic_palette(num_colors=num) + print(f"Chaotic {num}-Color Palette (Color Names):", palette) diff --git a/make_palette_complementary.py b/make_palette_complementary.py new file mode 100644 index 0000000..b0c0c21 --- /dev/null +++ b/make_palette_complementary.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +import sys +import random +import colorsys +import math +from .color_data import COLORS, color_families + +def hex_to_rgb(hex_code): + hex_code = hex_code.lstrip('#') + r = int(hex_code[0:2], 16) / 255.0 + g = int(hex_code[2:4], 16) / 255.0 + b = int(hex_code[4:6], 16) / 255.0 + return (r, g, b) + +def rgb_to_hex(rgb): + return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255)) + +def hex_to_rgb_tuple(hex_code): + hex_code = hex_code.lstrip('#') + return (int(hex_code[0:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)) + +def color_distance(rgb1, rgb2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2))) + +def find_closest_color_name(target_hex, color_dict): + target_rgb = hex_to_rgb_tuple(target_hex) + closest_name = None + min_dist = float('inf') + for name, hex_code in color_dict.items(): + current_rgb = hex_to_rgb_tuple(hex_code) + dist = color_distance(target_rgb, current_rgb) + if dist < min_dist: + min_dist = dist + closest_name = name + return closest_name + +def shift_hue(hex_code, degree_offset): + r, g, b = hex_to_rgb(hex_code) + H, L, S = colorsys.rgb_to_hls(r, g, b) + offset = degree_offset / 360.0 + new_H = (H + offset) % 1.0 + new_rgb = colorsys.hls_to_rgb(new_H, L, S) + return rgb_to_hex(new_rgb) + +def complementary_palette(base_hex): + """Returns 4 colors: base, complement, analogous (+15°), complement of analogous.""" + base = base_hex + comp = shift_hue(base_hex, 180) + analogous = shift_hue(base_hex, 15) + comp_analogous = shift_hue(analogous, 180) + return [base, comp, analogous, comp_analogous] + +def complementary_palette_5(base_hex): + """ + Returns 5 colors: + [base, complement, analogous (+15°), extra analogous variant (-15°), complement-of-analogous (+15°)] + """ + base = base_hex + comp = shift_hue(base_hex, 180) + analogous = shift_hue(base_hex, 15) + extra = shift_hue(base_hex, -15) + comp_analogous = shift_hue(analogous, 180) + return [base, comp, analogous, extra, comp_analogous] + +def generate_random_base_color(): + families = ['Reds', 'Pinks', 'Oranges', 'Yellows', 'Greens', 'Blues', 'Purples'] + candidate_colors = [] + for fam in families: + candidate_colors.extend(color_families.get(fam, [])) + name = random.choice(candidate_colors) + return name, COLORS[name] + +def generate_complementary_palette(): + """Generate 4-color complementary palette ensuring unique names.""" + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + palette_hexes = complementary_palette(base_hex) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 4: + return palette_names + + # Fallback: If we can't get 4 unique colors after max attempts, + # start with base and complement, then add unique colors + _, base_hex = generate_random_base_color() + comp_hex = shift_hue(base_hex, 180) + base_name = find_closest_color_name(base_hex, COLORS) + comp_name = find_closest_color_name(comp_hex, COLORS) + + # Start with base and complement + palette = [base_name, comp_name] + + # Try to add two more unique colors + all_colors = list(COLORS.keys()) + while len(palette) < 4 and all_colors: + candidate = random.choice(all_colors) + if candidate not in palette: + palette.append(candidate) + all_colors.remove(candidate) + + # If we still don't have 4 colors, pad with duplicates + while len(palette) < 4: + palette.append(palette[-1]) + + return palette + +def generate_complementary_palette_5(): + """Generate 5-color complementary palette ensuring unique names.""" + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + palette_hexes = complementary_palette_5(base_hex) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 5: + return palette_names + + # Fallback: If we can't get 5 unique colors after max attempts, + # start with base and complement, then add unique colors + _, base_hex = generate_random_base_color() + comp_hex = shift_hue(base_hex, 180) + base_name = find_closest_color_name(base_hex, COLORS) + comp_name = find_closest_color_name(comp_hex, COLORS) + + # Start with base and complement + palette = [base_name, comp_name] + + # Try to add three more unique colors + all_colors = list(COLORS.keys()) + while len(palette) < 5 and all_colors: + candidate = random.choice(all_colors) + if candidate not in palette: + palette.append(candidate) + all_colors.remove(candidate) + + # If we still don't have 5 colors, pad with duplicates + while len(palette) < 5: + palette.append(palette[-1]) + + return palette + +if __name__ == '__main__': + five_flag = '-5' in sys.argv + if five_flag: + palette = generate_complementary_palette_5() + else: + palette = generate_complementary_palette() + print("Complementary Palette (Color Names):", palette) \ No newline at end of file diff --git a/make_palette_double_complementary.py b/make_palette_double_complementary.py new file mode 100644 index 0000000..280fe38 --- /dev/null +++ b/make_palette_double_complementary.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +import sys +import random +import colorsys +import math +from .color_data import COLORS, color_families + +# ---------------------------- +# Conversion & Utility Functions +# ---------------------------- +def hex_to_rgb(hex_code): + hex_code = hex_code.lstrip('#') + r = int(hex_code[0:2], 16) / 255.0 + g = int(hex_code[2:4], 16) / 255.0 + b = int(hex_code[4:6], 16) / 255.0 + return (r, g, b) + +def rgb_to_hex(rgb): + return '#{:02x}{:02x}{:02x}'.format( + int(rgb[0] * 255), + int(rgb[1] * 255), + int(rgb[2] * 255) + ) + +def hex_to_rgb_tuple(hex_code): + hex_code = hex_code.lstrip('#') + return (int(hex_code[0:2], 16), + int(hex_code[2:4], 16), + int(hex_code[4:6], 16)) + +def color_distance(rgb1, rgb2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2))) + +def find_closest_color_name(target_hex, color_dict): + target_rgb = hex_to_rgb_tuple(target_hex) + closest_name = None + min_dist = float('inf') + for name, hex_code in color_dict.items(): + current_rgb = hex_to_rgb_tuple(hex_code) + dist = color_distance(target_rgb, current_rgb) + if dist < min_dist: + min_dist = dist + closest_name = name + return closest_name + +def shift_hue(hex_code, degree_offset): + r, g, b = hex_to_rgb(hex_code) + H, L, S = colorsys.rgb_to_hls(r, g, b) + offset = degree_offset / 360.0 + new_H = (H + offset) % 1.0 + new_rgb = colorsys.hls_to_rgb(new_H, L, S) + return rgb_to_hex(new_rgb) + +# ---------------------------- +# Double Complementary (Tetradic) Palette Functions +# ---------------------------- +def tetradic_palette(hex_code, shift_fraction=1/6): + """ + Generate a tetradic palette from a base hex color. + Returns 4 hex color codes that form two complementary pairs. + """ + base_rgb = hex_to_rgb(hex_code) + base_h, base_l, base_s = colorsys.rgb_to_hls(*base_rgb) + h1 = base_h + h2 = (base_h + 0.5) % 1.0 + h3 = (base_h + shift_fraction) % 1.0 + h4 = (base_h + shift_fraction + 0.5) % 1.0 + return [rgb_to_hex(colorsys.hls_to_rgb(h1, base_l, base_s)), + rgb_to_hex(colorsys.hls_to_rgb(h2, base_l, base_s)), + rgb_to_hex(colorsys.hls_to_rgb(h3, base_l, base_s)), + rgb_to_hex(colorsys.hls_to_rgb(h4, base_l, base_s))] + +def generate_random_base_color(): + families = ['Reds', 'Pinks', 'Oranges', 'Yellows', 'Greens', 'Blues', 'Purples'] + candidate_colors = [] + for fam in families: + candidate_colors.extend(color_families.get(fam, [])) + random_color_name = random.choice(candidate_colors) + return random_color_name, COLORS[random_color_name] + +def generate_double_complementary_palette_4(): + """Generate a 4-color double complementary (tetradic) palette.""" + greys = set(color_families.get("Greys", [])) + while True: + _, base_hex = generate_random_base_color() + palette_hexes = tetradic_palette(base_hex) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 4 and not any(palette_names.count(n) > 1 for n in greys): + return palette_names + +def generate_double_complementary_palette_5(): + """ + Generate a 5-color double complementary (tetradic) palette. + We start with the 4-color palette and add an extra analogous variant of the base (e.g. base+30°) + if it’s unique. + """ + while True: + base_name, base_hex = generate_random_base_color() + base_palette = tetradic_palette(base_hex) + extra = shift_hue(base_hex, 30) + palette_hexes = base_palette + [extra] + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 5: + return palette_names + +if __name__ == '__main__': + five_flag = '-5' in sys.argv + if five_flag: + palette = generate_double_complementary_palette_5() + else: + palette = generate_double_complementary_palette_4() + print("Double Complementary Palette (Color Names):", palette) diff --git a/make_palette_modified_compound.py b/make_palette_modified_compound.py new file mode 100644 index 0000000..74a8a0b --- /dev/null +++ b/make_palette_modified_compound.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +import sys +import random +import colorsys +import math +from .color_data import COLORS, color_families + +# ---------------------------- +# Conversion & Utility Functions +# ---------------------------- +def hex_to_rgb(hex_code): + hex_code = hex_code.lstrip('#') + r = int(hex_code[0:2], 16) / 255.0 + g = int(hex_code[2:4], 16) / 255.0 + b = int(hex_code[4:6], 16) / 255.0 + return (r, g, b) + +def rgb_to_hex(rgb): + return '#{:02x}{:02x}{:02x}'.format( + int(rgb[0] * 255), + int(rgb[1] * 255), + int(rgb[2] * 255) + ) + +def hex_to_rgb_tuple(hex_code): + hex_code = hex_code.lstrip('#') + return (int(hex_code[0:2], 16), + int(hex_code[2:4], 16), + int(hex_code[4:6], 16)) + +def color_distance(rgb1, rgb2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2))) + +def find_closest_color_name(target_hex, color_dict): + target_rgb = hex_to_rgb_tuple(target_hex) + closest_name = None + min_dist = float('inf') + for name, hex_code in color_dict.items(): + current_rgb = hex_to_rgb_tuple(hex_code) + dist = color_distance(target_rgb, current_rgb) + if dist < min_dist: + min_dist = dist + closest_name = name + return closest_name + +def shift_hue(hex_code, degree_offset): + r, g, b = hex_to_rgb(hex_code) + H, L, S = colorsys.rgb_to_hls(r, g, b) + offset = degree_offset / 360.0 + new_H = (H + offset) % 1.0 + new_rgb = colorsys.hls_to_rgb(new_H, L, S) + return rgb_to_hex(new_rgb) + +# ---------------------------- +# Compound (Double Split-Complementary) 5-Color Palette Function +# ---------------------------- +def compound_palette_five(base_hex, analogous_offset=15, split_offset=30): + color1 = base_hex + color2 = shift_hue(base_hex, analogous_offset) + color3 = shift_hue(base_hex, 180 - split_offset) + color4 = shift_hue(base_hex, 180 + split_offset) + color5 = shift_hue(base_hex, -analogous_offset) + return [color1, color2, color3, color4, color5] + +def generate_random_base_color(): + families = ['Reds', 'Pinks', 'Oranges', 'Yellows', 'Greens', 'Blues', 'Purples'] + candidate_colors = [] + for fam in families: + candidate_colors.extend(color_families.get(fam, [])) + name = random.choice(candidate_colors) + return name, COLORS[name] + +def generate_modified_compound_palette_five(): + """ + Generate a 5-color compound (double split-complementary) palette. + """ + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + palette_hexes = compound_palette_five(base_hex, analogous_offset=15, split_offset=30) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 5: + return palette_names + + # Fallback: If we can't get 5 unique colors after max attempts, + # try with different offset values + for offset_mult in [1.2, 1.5, 2.0]: # Try larger offsets + _, base_hex = generate_random_base_color() + palette_hexes = compound_palette_five( + base_hex, + analogous_offset=15 * offset_mult, + split_offset=30 * offset_mult + ) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 5: + return palette_names + + # Ultimate fallback - generate any 5 unique colors + all_colors = list(COLORS.keys()) + palette = [] + while len(palette) < 5 and all_colors: + candidate = random.choice(all_colors) + if candidate not in palette: + palette.append(candidate) + all_colors.remove(candidate) + + # If we still don't have 5 colors, pad with duplicates + while len(palette) < 5: + palette.append(palette[-1]) + + return palette + +def generate_modified_compound_palette_four_from_five(): + """ + Generate a 4-color palette by randomly sampling 4 colors from the 5-color compound palette. + """ + max_attempts = 50 + + for _ in range(max_attempts): + five_palette = generate_modified_compound_palette_five() + four_palette = random.sample(five_palette, 4) + if len(set(four_palette)) == 4: + return four_palette + + # Fallback: If we can't get 4 unique colors after max attempts, + # generate a new 5-color palette and take the first 4 unique colors + five_palette = generate_modified_compound_palette_five() + unique_colors = [] + seen = set() + + for color in five_palette: + if color not in seen and len(unique_colors) < 4: + unique_colors.append(color) + seen.add(color) + + # If we still don't have 4 colors, pad with duplicates + while len(unique_colors) < 4: + unique_colors.append(unique_colors[-1]) + + return unique_colors + +if __name__ == '__main__': + use_five = '-5' in sys.argv + if use_five: + palette = generate_modified_compound_palette_five() + else: + palette = generate_modified_compound_palette_four_from_five() + print("Compound Palette (Color Names):", palette) \ No newline at end of file diff --git a/make_palette_split_complementary.py b/make_palette_split_complementary.py new file mode 100644 index 0000000..d528960 --- /dev/null +++ b/make_palette_split_complementary.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +import sys +import random +import colorsys +import math +from .color_data import COLORS, color_families + +# ---------------------------- +# Conversion & Utility Functions +# ---------------------------- +def hex_to_rgb(hex_code): + hex_code = hex_code.lstrip('#') + r = int(hex_code[0:2], 16) / 255.0 + g = int(hex_code[2:4], 16) / 255.0 + b = int(hex_code[4:6], 16) / 255.0 + return (r, g, b) + +def rgb_to_hex(rgb): + return '#{:02x}{:02x}{:02x}'.format( + int(rgb[0]*255), + int(rgb[1]*255), + int(rgb[2]*255) + ) + +def hex_to_rgb_tuple(hex_code): + hex_code = hex_code.lstrip('#') + return (int(hex_code[0:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)) + +def color_distance(rgb1, rgb2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2))) + +def find_closest_color_name(target_hex, color_dict): + target_rgb = hex_to_rgb_tuple(target_hex) + closest_name = None + min_dist = float('inf') + for name, hex_code in color_dict.items(): + current_rgb = hex_to_rgb_tuple(hex_code) + dist = color_distance(target_rgb, current_rgb) + if dist < min_dist: + min_dist = dist + closest_name = name + return closest_name + +def shift_hue(hex_code, degree_offset): + r, g, b = hex_to_rgb(hex_code) + H, L, S = colorsys.rgb_to_hls(r, g, b) + offset = degree_offset / 360.0 + new_H = (H + offset) % 1.0 + new_rgb = colorsys.hls_to_rgb(new_H, L, S) + return rgb_to_hex(new_rgb) + +# ---------------------------- +# Split-Complementary Palette Functions +# ---------------------------- +def split_complementary_palette_4(base_hex, split_offset_degrees=30, analogous_offset_degrees=15): + """ + Returns 4 colors: + - Color 1: Base. + - Colors 2 & 3: Two split-complementary variants (computed from the base's complement). + - Color 4: Chosen by a 50/50 decision: either an analogous variant of the base (using +15°) + or a random color from Broad Neutrals. + """ + # Convert base to HLS. + base_rgb = hex_to_rgb(base_hex) + H, L, S = colorsys.rgb_to_hls(*base_rgb) + # Complement of base. + comp_H = (H + 0.5) % 1.0 + # Compute split offsets. + offset = split_offset_degrees / 360.0 + split1_H = (comp_H + offset) % 1.0 + split2_H = (comp_H - offset) % 1.0 + color1 = rgb_to_hex(colorsys.hls_to_rgb(H, L, S)) + color2 = rgb_to_hex(colorsys.hls_to_rgb(split1_H, L, S)) + color3 = rgb_to_hex(colorsys.hls_to_rgb(split2_H, L, S)) + # Decide on fourth color. + use_analogous = random.choice([True, False]) + broad_neutrals = color_families.get("Broad Neutrals", []) + # Check if any of the first three (by name) are in Broad Neutrals. + first3_names = {find_closest_color_name(c, COLORS) for c in [color1, color2, color3]} + if not use_analogous and any(n in broad_neutrals for n in first3_names): + use_analogous = True + if use_analogous: + color4 = shift_hue(base_hex, analogous_offset_degrees) + else: + random_neutral = random.choice(broad_neutrals) + color4 = COLORS[random_neutral] + return [color1, color2, color3, color4] + +def split_complementary_palette_5(base_hex, split_offset_degrees=30, analogous_offset_degrees=15): + """ + Returns 5 colors. We start with the 4-color split-complementary palette and then add an extra color. + The extra color is chosen as follows: + - If the 4-color version used an analogous variant for the fourth color, we add the opposite analogous variant + (i.e. base shifted by -analogous_offset_degrees) if it isn't already present. + - Otherwise (if a Broad Neutral was chosen), we add a second random Broad Neutral (that is unique). + """ + max_attempts = 50 # Prevent infinite loops + for attempt in range(max_attempts): + base_palette = split_complementary_palette_4(base_hex, split_offset_degrees, analogous_offset_degrees) + # Determine what the fourth color was. + fourth_name = find_closest_color_name(base_palette[3], COLORS) + extra_color = None + + if fourth_name in color_families.get("Broad Neutrals", []): + # Fourth was a Broad Neutral; pick another different one. + current_names = [find_closest_color_name(h, COLORS) for h in base_palette] + candidates = [n for n in color_families.get("Broad Neutrals", []) + if n not in current_names] + if candidates: + extra_color = COLORS[random.choice(candidates)] + else: + # Fourth was analogous; add the opposite analogous variant. + extra_color = shift_hue(base_hex, -analogous_offset_degrees) + + if extra_color is None: + continue + + # Verify extra_color is unique + full_palette = base_palette + [extra_color] + if len(set([find_closest_color_name(h, COLORS) for h in full_palette])) == 5: + return full_palette + + # If we couldn't generate a valid 5-color palette after max attempts, + # fall back to the 4-color palette plus a random color + base_palette = split_complementary_palette_4(base_hex, split_offset_degrees, analogous_offset_degrees) + all_colors = list(COLORS.keys()) + current_names = [find_closest_color_name(h, COLORS) for h in base_palette] + + while all_colors: + random_color = random.choice(all_colors) + if random_color not in current_names: + return base_palette + [COLORS[random_color]] + all_colors.remove(random_color) + + # Absolute fallback - just duplicate the last color if everything else fails + return base_palette + [base_palette[-1]] + +def generate_random_base_color(): + families = ['Reds', 'Pinks', 'Oranges', 'Yellows', 'Greens', 'Blues', 'Purples'] + candidate_colors = [] + for fam in families: + candidate_colors.extend(color_families.get(fam, [])) + random_color_name = random.choice(candidate_colors) + return random_color_name, COLORS[random_color_name] + +def generate_split_complementary_palette_4(): + """Generate a 4-color split-complementary palette ensuring unique names.""" + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + palette_hexes = split_complementary_palette_4(base_hex) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 4: + return palette_names + + # Fallback: Try with different offset values + for split_offset in [45, 60]: # Try larger split offsets + _, base_hex = generate_random_base_color() + palette_hexes = split_complementary_palette_4(base_hex, split_offset_degrees=split_offset) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 4: + return palette_names + + # Ultimate fallback - generate any 4 unique colors + all_colors = list(COLORS.keys()) + palette = [] + while len(palette) < 4 and all_colors: + candidate = random.choice(all_colors) + if candidate not in palette: + palette.append(candidate) + all_colors.remove(candidate) + + # If we still don't have 4 colors, pad with duplicates + while len(palette) < 4: + palette.append(palette[-1]) + + return palette + +def generate_split_complementary_palette_5(): + """Generate a 5-color split-complementary palette ensuring unique names.""" + max_attempts = 50 + + for _ in range(max_attempts): + _, base_hex = generate_random_base_color() + palette_hexes = split_complementary_palette_5(base_hex) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 5: + return palette_names + + # Fallback: Try with different offset values + for split_offset in [45, 60]: # Try larger split offsets + _, base_hex = generate_random_base_color() + palette_hexes = split_complementary_palette_5(base_hex, split_offset_degrees=split_offset) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + if len(set(palette_names)) == 5: + return palette_names + + # Ultimate fallback - generate any 5 unique colors + all_colors = list(COLORS.keys()) + palette = [] + while len(palette) < 5 and all_colors: + candidate = random.choice(all_colors) + if candidate not in palette: + palette.append(candidate) + all_colors.remove(candidate) + + # If we still don't have 5 colors, pad with duplicates + while len(palette) < 5: + palette.append(palette[-1]) + + return palette + +if __name__ == '__main__': + five_flag = '-5' in sys.argv + if five_flag: + palette = generate_split_complementary_palette_5() + else: + palette = generate_split_complementary_palette_4() + print("Split-Complementary Palette (Color Names):", palette) \ No newline at end of file diff --git a/make_palette_triadic.py b/make_palette_triadic.py new file mode 100644 index 0000000..edd1626 --- /dev/null +++ b/make_palette_triadic.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +import sys +import random +import math +import colorsys +from .color_data import COLORS, color_families + +# ---------------------------- +# Conversion & Utility Functions +# ---------------------------- +def hex_to_rgb(hex_code): + """Convert a hex color code to an RGB tuple with components in the range [0, 1].""" + hex_code = hex_code.lstrip('#') + if len(hex_code) != 6: + raise ValueError("Hex code must be 6 digits long.") + r = int(hex_code[0:2], 16) / 255.0 + g = int(hex_code[2:4], 16) / 255.0 + b = int(hex_code[4:6], 16) / 255.0 + return (r, g, b) + +def rgb_to_hex(rgb): + """Convert an RGB tuple with components in the range [0, 1] to a hex color code.""" + return '#{:02x}{:02x}{:02x}'.format( + int(rgb[0] * 255), + int(rgb[1] * 255), + int(rgb[2] * 255) + ) + +def hex_to_rgb_tuple(hex_code): + """Convert a hex code to an (R, G, B) tuple with values 0-255.""" + hex_code = hex_code.lstrip('#') + return ( + int(hex_code[0:2], 16), + int(hex_code[2:4], 16), + int(hex_code[4:6], 16) + ) + +def color_distance(rgb1, rgb2): + """Compute the Euclidean distance between two RGB colors (in 0-255 space).""" + return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2))) + +def find_closest_color_name(target_hex, color_dict): + """ + Given a target hex code and a dictionary mapping color names to hex codes, + return the name of the closest matching color based on Euclidean distance in RGB space. + """ + target_rgb = hex_to_rgb_tuple(target_hex) + closest_name = None + min_dist = float('inf') + for name, hex_code in color_dict.items(): + current_rgb = hex_to_rgb_tuple(hex_code) + dist = color_distance(target_rgb, current_rgb) + if dist < min_dist: + min_dist = dist + closest_name = name + return closest_name + +def shift_hue(hex_code, degree_offset=15): + """ + Shifts the hue of the given hex color by the specified degree offset. + The degree_offset is added to the hue (in degrees) and wrapped around if necessary. + """ + r, g, b = hex_to_rgb(hex_code) + h, l, s = colorsys.rgb_to_hls(r, g, b) + offset = degree_offset / 360.0 + new_h = (h + offset) % 1.0 + new_rgb = colorsys.hls_to_rgb(new_h, l, s) + return rgb_to_hex(new_rgb) + +# ---------------------------- +# Triadic Palette Functions +# ---------------------------- +def triadic_palette(base_hex): + """ + Given a base hex color code, returns a list of three hex color codes that form a triadic palette. + The palette consists of the original color and two additional colors that are 120° apart. + """ + base_rgb = hex_to_rgb(base_hex) + base_h, base_l, base_s = colorsys.rgb_to_hls(*base_rgb) + hue2 = (base_h + 1/3) % 1.0 + hue3 = (base_h + 2/3) % 1.0 + color2 = rgb_to_hex(colorsys.hls_to_rgb(hue2, base_l, base_s)) + color3 = rgb_to_hex(colorsys.hls_to_rgb(hue3, base_l, base_s)) + return [base_hex, color2, color3] + +def generate_random_triadic_palette(): + """ + 1. Picks a random color from the families: Reds, Pinks, Oranges, Yellows, Greens, Blues, Purples. + 2. Generates a triadic palette (3 colors) using the selected color. + 3. Converts the hex codes to their closest color names. + 4. Returns a list of 3 color names. + """ + families = ['Reds', 'Pinks', 'Oranges', 'Yellows', 'Greens', 'Blues', 'Purples'] + candidate_colors = [] + for fam in families: + candidate_colors.extend(color_families.get(fam, [])) + random_color_name = random.choice(candidate_colors) + base_hex = COLORS[random_color_name] + palette_hexes = triadic_palette(base_hex) + palette_names = [find_closest_color_name(h, COLORS) for h in palette_hexes] + return palette_names + +def generate_four_color_palette(): + """ + Builds on the 3-color triadic palette to add a fourth color. + The fourth color is chosen from one of three methods (analogous, tetradic, or Broad Neutral) + so that the final palette has 4 unique color names. + """ + max_attempts = 50 + for _ in range(max_attempts): + triadic_names = generate_random_triadic_palette() + base_color_name = triadic_names[0] + base_hex = COLORS[base_color_name] + + allowed_methods = ['analogous', 'tetradic'] + broad_neutrals = color_families.get('Broad Neutrals', []) + if not any(name in broad_neutrals for name in triadic_names): + allowed_methods.append('broad_neutral') + + methods_to_try = allowed_methods.copy() + fourth_color_name = None + + while methods_to_try: + chosen_method = random.choice(methods_to_try) + if chosen_method == 'analogous': + fourth_hex = shift_hue(base_hex, degree_offset=15) + elif chosen_method == 'tetradic': + fourth_hex = shift_hue(base_hex, degree_offset=180) + elif chosen_method == 'broad_neutral': + fourth_name_candidate = random.choice(broad_neutrals) + fourth_hex = COLORS[fourth_name_candidate] + else: + raise ValueError("Unknown method selected.") + + candidate_name = find_closest_color_name(fourth_hex, COLORS) + if candidate_name not in triadic_names: + fourth_color_name = candidate_name + break + else: + methods_to_try.remove(chosen_method) + + if fourth_color_name is not None: + palette = triadic_names + [fourth_color_name] + if len(set(palette)) == 4: + return palette + + # Fallback: If we can't get 4 unique colors after max attempts, + # start with triadic palette and add any unique color + triadic_names = generate_random_triadic_palette() + all_colors = list(COLORS.keys()) + while all_colors: + random_color = random.choice(all_colors) + if random_color not in triadic_names: + return triadic_names + [random_color] + all_colors.remove(random_color) + + # Ultimate fallback - duplicate the last color if everything else fails + return triadic_names + [triadic_names[-1]] + +def generate_triadic_palette_5(): + """ + Builds on the 3-color triadic palette to add two extra colors for a total of 5. + For each extra slot, there is an independent 50% chance of selecting a color from Broad Neutrals; + otherwise, an analogous variant is generated. + """ + max_attempts = 50 + for _ in range(max_attempts): + triadic_names = generate_random_triadic_palette() + base_color_name = triadic_names[0] + base_hex = COLORS[base_color_name] + broad_neutrals = color_families.get("Broad Neutrals", []) + current_palette = triadic_names.copy() + + # Try to add two extra colors + for i in range(2): + if random.random() < 0.5 and broad_neutrals: + # Filter out colors already in the palette + available_neutrals = [n for n in broad_neutrals if n not in current_palette] + if available_neutrals: + extra_name = random.choice(available_neutrals) + current_palette.append(extra_name) + continue + + # If we couldn't use a neutral or random chose analogous + offset = 15 if i == 0 else -15 + extra_hex = shift_hue(base_hex, degree_offset=offset) + extra_name = find_closest_color_name(extra_hex, COLORS) + if extra_name not in current_palette: + current_palette.append(extra_name) + + if len(current_palette) == 5 and len(set(current_palette)) == 5: + return current_palette + + # Fallback: If we can't get 5 unique colors, + # start with triadic palette and add unique random colors + triadic_names = generate_random_triadic_palette() + current_palette = triadic_names.copy() + all_colors = list(COLORS.keys()) + + while len(current_palette) < 5 and all_colors: + random_color = random.choice(all_colors) + if random_color not in current_palette: + current_palette.append(random_color) + all_colors.remove(random_color) + + # If we still don't have 5 colors, pad with duplicates + while len(current_palette) < 5: + current_palette.append(current_palette[-1]) + + return current_palette + +# ---------------------------- +# Main Execution +# ---------------------------- +if __name__ == "__main__": + use_five = '-5' in sys.argv + if use_five: + palette = generate_triadic_palette_5() + else: + palette = generate_four_color_palette() + print("Triadic Palette (Color Names):", palette) \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..05d4c2f --- /dev/null +++ b/nodes.py @@ -0,0 +1,833 @@ +import random +import re +import os +from datetime import datetime, timedelta +from .weather_utils import generate_weather_description +from .color_data import COLORS, color_families +# Palette generation module imports: +from .make_palette_analogous import generate_analogous_palette_4, generate_analogous_palette_5 +from .make_palette_art_house import generate_art_house_palette_4, generate_art_house_palette_5 +from .make_palette_chaotic import generate_chaotic_palette +from .make_palette_complementary import generate_complementary_palette, generate_complementary_palette_5 +from .make_palette_double_complementary import generate_double_complementary_palette_4, generate_double_complementary_palette_5 +from .make_palette_modified_compound import generate_modified_compound_palette_five, generate_modified_compound_palette_four_from_five +from .make_palette_split_complementary import generate_split_complementary_palette_4, generate_split_complementary_palette_5 +from .make_palette_triadic import generate_four_color_palette, generate_triadic_palette_5 + +class EbuPromptHelperRandomColorPalette: + """ + EBU Random Color Palette Generator Node + Inputs: + - include_analogous_palettes (BOOLEAN): Include analogous palettes (default True) + - include_art_house_palettes (BOOLEAN): Include art house palettes (default True) + - include_chaotic_palettes (BOOLEAN): Include chaotic palettes (default True) + - include_complementary_palettes (BOOLEAN): Include complementary palettes (default True) + - include_compound_palettes (BOOLEAN): Include compound palettes (default True) + - include_split_complementary_palettes (BOOLEAN): Include split complementary palettes (default True) + - include_tetradic_palettes (BOOLEAN): Include tetradic palettes (default True) + - include_triadic_palettes (BOOLEAN): Include triadic palettes (default True) + - palette_size (STRING): Drop‑down selection for palette size. Options: + "3 colors", "4 colors", or "5 colors". + (If "3 colors" is selected, a standard 4‑color palette is generated and its first 3 colors are returned.) + - prefer_color_family (STRING): Drop‑down for a preferred color family. + Options: "None", "Reds", "Pinks", "Oranges", "Yellows", "Greens", "Blues", "Purples", + "Browns", "Greys", "Neutrals", "Metallics", "Pastels", "Peaches", "Warm Colors", "Cool Colors". + (If not "None", the palette is regenerated up to 50 times until the condition is met.) + - avoid_color_family (STRING): Drop‑down for a color family to avoid. + Options: "None", "Reds", "Pinks", "Oranges", "Yellows", "Greens", "Blues", "Purples", + "Browns", "Greys", "Neutrals", "Metallics", "Pastels", "Peaches", "Warm Colors", "Cool Colors". + (If not "None", the palette is regenerated up to 50 times until the condition is met.) + - seed (INT): Seed for random generation (0 means non‑deterministic). + Returns: + - palette (STRING): A comma‑separated list of color names (always at least 3), in lower-case. + - color1 (STRING): The first color in the palette (lower-case). + - color2 (STRING): The second color in the palette (lower-case). + - color3 (STRING): The third color in the palette (lower-case). + - color4 (STRING): The fourth color if available, or "" if not. + - color5 (STRING): The fifth color if available, or "" if not. + - palette_type (STRING): The chosen palette type. + - hex_values (STRING): A comma‑separated list of hex values corresponding to each color. + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "include_analogous_palettes": ("BOOLEAN", {"default": True}), + "include_art_house_palettes": ("BOOLEAN", {"default": True}), + "include_chaotic_palettes": ("BOOLEAN", {"default": True}), + "include_complementary_palettes": ("BOOLEAN", {"default": True}), + "include_compound_palettes": ("BOOLEAN", {"default": True}), + "include_split_complementary_palettes": ("BOOLEAN", {"default": True}), + "include_tetradic_palettes": ("BOOLEAN", {"default": True}), + "include_triadic_palettes": ("BOOLEAN", {"default": True}), + "palette_size": (["3 colors", "4 colors", "5 colors"], {"default": "4 colors"}), + "prefer_color_family": ( + ["None", "Reds", "Pinks", "Oranges", "Yellows", "Greens", "Blues", "Purples", + "Browns", "Greys", "Neutrals", "Metallics", "Pastels", "Peaches", "Warm Colors", "Cool Colors"], + {"default": "None"} + ), + "avoid_color_family": ( + ["None", "Reds", "Pinks", "Oranges", "Yellows", "Greens", "Blues", "Purples", + "Browns", "Greys", "Neutrals", "Metallics", "Pastels", "Peaches", "Warm Colors", "Cool Colors"], + {"default": "None"} + ), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("palette_string", "color1", "color2", "color3", "color4", "color5", "palette_type", "hex_values") + FUNCTION = "generate_palette" + CATEGORY = "Prompts" + + def generate_palette( + self, + include_analogous_palettes, + include_art_house_palettes, + include_chaotic_palettes, + include_complementary_palettes, + include_compound_palettes, + include_split_complementary_palettes, + include_tetradic_palettes, + include_triadic_palettes, + palette_size, + prefer_color_family, + avoid_color_family, + seed): + + # Set the random seed. + random.seed(seed) + + # If the same color family is specified for both prefer and avoid, cancel them out. + if (prefer_color_family != "None" and + avoid_color_family != "None" and + prefer_color_family == avoid_color_family): + print("Warning: The same color family was selected for both prefer and avoid. Cancelling these conditions.") + prefer_color_family = "None" + avoid_color_family = "None" + + # Build a list of enabled palette types. + enabled_types = [] + if include_analogous_palettes: + enabled_types.append("analogous") + if include_art_house_palettes: + enabled_types.append("art house") + if include_chaotic_palettes: + enabled_types.append("chaotic") + if include_complementary_palettes: + enabled_types.append("complementary") + if include_compound_palettes: + enabled_types.append("compound") + if include_split_complementary_palettes: + enabled_types.append("split complementary") + if include_tetradic_palettes: + enabled_types.append("tetradic") + if include_triadic_palettes: + enabled_types.append("triadic") + + # If none are enabled, log a warning and default to all palette types. + if not enabled_types: + print("Warning: No palette types were enabled. Defaulting to all palette types.") + enabled_types = [ + "analogous", "art house", "chaotic", "complementary", + "compound", "split complementary", "tetradic", "triadic" + ] + + # Determine target palette size (numeric). + if palette_size == "3 colors": + target_size = 3 + elif palette_size == "4 colors": + target_size = 4 + elif palette_size == "5 colors": + target_size = 5 + else: + raise Exception("Invalid palette size: " + palette_size) + + max_attempts = 50 + chosen_palette = None + chosen_type = None # To record which palette type was chosen. + + for attempt in range(max_attempts): + chosen_type = random.choice(enabled_types) + if chosen_type == "analogous": + if target_size == 5: + palette = generate_analogous_palette_5() + else: + palette = generate_analogous_palette_4() + if target_size == 3: + palette = palette[:3] + elif chosen_type == "art house": + if target_size == 5: + palette = generate_art_house_palette_5() + else: + palette = generate_art_house_palette_4() + if target_size == 3: + palette = palette[:3] + elif chosen_type == "chaotic": + if target_size == 5: + palette = generate_chaotic_palette(num_colors=5) + elif target_size == 4: + palette = generate_chaotic_palette(num_colors=4) + elif target_size == 3: + palette = generate_chaotic_palette(num_colors=4)[:3] + elif chosen_type == "complementary": + if target_size == 5: + palette = generate_complementary_palette_5() + else: + palette = generate_complementary_palette() + if target_size == 3: + palette = palette[:3] + elif chosen_type == "compound": + if target_size == 5: + palette = generate_modified_compound_palette_five() + else: + palette = generate_modified_compound_palette_four_from_five() + if target_size == 3: + palette = palette[:3] + elif chosen_type == "split complementary": + if target_size == 5: + palette = generate_split_complementary_palette_5() + else: + palette = generate_split_complementary_palette_4() + if target_size == 3: + palette = palette[:3] + elif chosen_type == "tetradic": + if target_size == 5: + palette = generate_double_complementary_palette_5() + else: + palette = generate_double_complementary_palette_4() + if target_size == 3: + palette = palette[:3] + elif chosen_type == "triadic": + if target_size == 5: + palette = generate_triadic_palette_5() + else: + palette = generate_four_color_palette() + if target_size == 3: + palette = palette[:3] + else: + raise Exception("Unknown palette type chosen: " + chosen_type) + + # --- Check the "prefer" condition --- + condition_preferred = True + if prefer_color_family != "None": + if prefer_color_family in ["Warm Colors", "Cool Colors"]: + warm_list = color_families.get("Warm Colors", []) + cool_list = color_families.get("Cool Colors", []) + warm_count = sum(1 for color in palette if color in warm_list) + cool_count = sum(1 for color in palette if color in cool_list) + if prefer_color_family == "Warm Colors": + condition_preferred = (warm_count >= cool_count + 1) + else: + condition_preferred = (cool_count >= warm_count + 1) + else: + preferred_list = color_families.get(prefer_color_family, []) + condition_preferred = any(color in preferred_list for color in palette) + + # --- Check the "avoid" condition --- + condition_avoid = True + if avoid_color_family != "None": + if avoid_color_family in ["Warm Colors", "Cool Colors"]: + warm_list = color_families.get("Warm Colors", []) + cool_list = color_families.get("Cool Colors", []) + warm_count = sum(1 for color in palette if color in warm_list) + cool_count = sum(1 for color in palette if color in cool_list) + if avoid_color_family == "Warm Colors": + condition_avoid = (warm_count <= cool_count - 1) + else: + condition_avoid = (cool_count <= warm_count - 1) + else: + avoid_list = color_families.get(avoid_color_family, []) + condition_avoid = not any(color in avoid_list for color in palette) + + if condition_preferred and condition_avoid: + chosen_palette = palette + break + + if chosen_palette is None: + print(f"Warning: Attempted {max_attempts} times to meet color preferences but wasn't able to.") + chosen_palette = palette + + display_palette = [color.lower() for color in chosen_palette] + palette_str = ", ".join(display_palette) + padded_display = display_palette.copy() + while len(padded_display) < 5: + padded_display.append("") + color1, color2, color3, color4, color5 = padded_display[:5] + hex_list = [COLORS.get(color, "") for color in chosen_palette] + hex_values_str = ", ".join(hex_list) + + return (palette_str, color1, color2, color3, color4, color5, chosen_type, hex_values_str) + +class EbuPromptHelperReplace: + """ + EBU Prompt Helper Replace Node + + This node replaces occurrences of one or more target words within an input prompt with a specified replacement string. + The target words should be provided as a single string, where multiple words are separated by the '|' character. + For example, if the `word_to_replace` input is "person|man|woman", then any occurrence of "person", "man", or "woman" + in the prompt will be replaced with the value provided in `replace_with`. + + Inputs: + - prompt_text (STRING): A multiline string representing the input prompt. + - word_to_replace (STRING): A single-line string containing one or more words separated by '|'. + - replace_with (STRING): A multiline string that will replace each occurrence of the target words. + - case_sensitive (BOOLEAN): A toggle to control if the replacement is case‑sensitive (default True). + """ + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt_text": ("STRING", {"multiline": True, "default": ""}), + "word_to_replace": ("STRING", {"multiline": False, "default": ""}), + "replace_with": ("STRING", {"multiline": True, "default": ""}), + "case_sensitive": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("updated_prompt_text",) + FUNCTION = "replace_text" + CATEGORY = "Example" + + def replace_text(self, prompt_text, word_to_replace, replace_with, case_sensitive): + if not word_to_replace.strip(): + return (prompt_text,) + words = word_to_replace.split("|") + revised = prompt_text + if case_sensitive: + for word in words: + revised = revised.replace(word, replace_with) + else: + import re + for word in words: + pattern = re.compile(re.escape(word), re.IGNORECASE) + revised = pattern.sub(replace_with, revised) + return (revised,) + +class EbuPromptHelperRandomize: + """ + EBU PromptHelper Randomize Node + + This node takes an input prompt and replaces occurrences of a specified target substring + (word_to_replace) with a randomly selected option from a provided string of options. + The options string is split based on the chosen delimiter, which can be newlines, commas, or semi‑colons. + The seed input controls the random selection, allowing for deterministic output if desired. + + Inputs: + - prompt_text (STRING): A multiline string representing the input prompt. + - word_to_replace (STRING): A single-line string indicating the substring to replace. + - replacement_options (STRING): A string containing replacement options. + - seed (INT): Ensures that node runs every time + - case_sensitive (BOOLEAN): A toggle to control if the replacement is case‑sensitive (default True). + - delimit_options_with (STRING): Dropdown with options ["newlines", "commas", "semi-colons"] + that determines how to split the options string. + + Returns: + - updated_prompt_text (STRING): The prompt after replacement. + - word_selected (STRING): The randomly selected replacement option. + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt_text": ("STRING", {"multiline": True, "default": ""}), + "word_to_replace": ("STRING", {"multiline": False, "default": ""}), + "replacement_options": ("STRING", {"multiline": True, "default": ""}), + "seed": ("INT", {"default": 0, "step": 1, "display": "number"}), + "case_sensitive": ("BOOLEAN", {"default": True}), + "delimit_options_with": (["newlines", "commas", "semi-colons"], {"default": "newlines"}) + } + } + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("updated_prompt_text", "word_selected") + FUNCTION = "randomize_text" + CATEGORY = "Prompts" + + def randomize_text(self, prompt_text, word_to_replace, replacement_options, seed, case_sensitive, delimit_options_with): + random.seed(seed) + + # Split the options string based on the selected delimiter. + if delimit_options_with == "newlines": + options = [opt.strip() for opt in replacement_options.splitlines() if opt.strip()] + elif delimit_options_with == "commas": + options = [opt.strip() for opt in replacement_options.split(',') if opt.strip()] + elif delimit_options_with == "semi-colons": + options = [opt.strip() for opt in replacement_options.split(';') if opt.strip()] + else: + options = [opt.strip() for opt in replacement_options.splitlines() if opt.strip()] + + if not options: + return (prompt_text, "") + + selected = random.choice(options) + + if case_sensitive: + new_prompt = prompt_text.replace(word_to_replace, selected) + else: + import re + pattern = re.compile(re.escape(word_to_replace), re.IGNORECASE) + new_prompt = pattern.sub(selected, prompt_text) + + return (new_prompt, selected) + +class EbuPromptHelperCombineTwoStrings: + """ + EBU PromptHelper Combine Two Strings Node + + This node takes two input strings and combines them into one string using a provided join string. + Only non-empty input strings are included in the final result. For example, if one of the inputs is + empty, only the non-empty string is returned. + + Inputs: + - str1 (STRING): The first string to combine. (Multiline supported.) + - str2 (STRING): The second string to combine. (Multiline supported.) + - join_str (STRING): The string used to join the two input strings. Defaults to "\n\n". + + Returns: + - combined (STRING): The resulting combined string. + """ + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "str1": ("STRING", { + "multiline": True, + "default": "" + }), + "str2": ("STRING", { + "multiline": True, + "default": "" + }), + "join_str": ("STRING", { + "multiline": True, + "default": "\n\n" + }), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("combined",) + + FUNCTION = "combine_two_strings" + + CATEGORY = "Prompts" + + def combine_two_strings(self, str1, str2, join_str): + strs = [] + if str1: + strs.append(str1) + if str2: + strs.append(str2) + combined = join_str.join(strs) + return (combined,) + +class EbuPromptHelperListSampler: + """ + EBU PromptHelper List Sampler Node + + This node takes a multi-line string (representing a list) as input, processes each line + by removing any leading numbers (e.g., "1. ", "23. ") and trimming whitespace, and then + randomly shuffles the remaining non-empty lines. It returns a sample containing a specified + number of lines (or all lines if there are fewer than requested). If the toggle is enabled, + the sampled list will be numbered starting at 1. + + Inputs: + - list (STRING): A multi-line string containing the list items. Each line may optionally + start with a number and a dot. + - seed (INT): A seed for the random number generator. Use 0 for non-deterministic output. + - number_of_elements (INT, optional): The maximum number of list elements to sample. Defaults to 10. + - number_sampled_list (BOOLEAN, optional): If True, the sampled list will be numbered (default False). + + Returns: + - (STRING): A newline-separated string of the sampled list items. + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "list": ("STRING", {"multiline": True}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + "optional": { + "number_of_elements": ("INT", {"default": 10, "min": 1, "max": 1000}), + "number_sampled_list": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "sample_list" + CATEGORY = "utils" + + def sample_list(self, list, seed, number_of_elements=10, number_sampled_list=False): + # Split the input string into lines. + lines = list.split('\n') + + # Process lines: remove leading numbers and ignore empty or whitespace-only lines. + processed_lines = [] + for line in lines: + # Remove leading numbers followed by a dot (e.g., "1. ", "23. "). + processed_line = re.sub(r'^\d+\.\s*', '', line) + processed_line = processed_line.strip() + if processed_line: # Only add non-empty lines. + processed_lines.append(processed_line) + + # Set the random seed. + random.seed(seed) + + # Shuffle the processed lines. + random.shuffle(processed_lines) + + # Select the specified number of elements, or all available if there are fewer. + selected_lines = processed_lines[:min(number_of_elements, len(processed_lines))] + + # If numbering is enabled, number each line starting at 1. + if number_sampled_list: + numbered_lines = [] + for idx, line in enumerate(selected_lines, start=1): + numbered_lines.append(f"{idx}. {line}") + selected_lines = numbered_lines + + # Join the selected lines into a single string separated by newlines. + output = '\n'.join(selected_lines) + + return (output,) + +class EbuPromptHelperLoadFileAsString: + """ + EBU PromptHelper Load File as String Node + + This node loads the contents of a file as a string. Provide the directory and file name, and the node + will attempt to read the file using UTF-8 encoding. If an error occurs (e.g., if the file is not found), + the node will print an error message and return an empty string. + + Inputs: + - directory (STRING): The directory path where the file is located. + - file_name (STRING): The name of the file to load. + + Returns: + - (STRING): The content of the file as a string, or an empty string if an error occurred. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "directory": ("STRING", {"default": ""}), + "file_name": ("STRING", {"default": ""}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "load_file" + CATEGORY = "File Operations" + + def load_file(self, directory, file_name): + full_path = os.path.join(directory, file_name) + try: + with open(full_path, 'r', encoding='utf-8') as file: + content = file.read() + return (content,) + except Exception as e: + print(f"Error reading file: {e}") + return ("",) + +class EbuPromptHelperCurrentDateTime: + """ + EBU PromptHelper Current DateTime Node + + This node returns the current datetime (at the moment of execution) in several formats: + + - date: "February 10, 2025" + - time: "1:15pm" + - datetime: "2025-02-10 1:15:33pm" + - datetime_for_filename: "2025-02-10_13-15-33" + + No input parameters are required. + """ + @classmethod + def INPUT_TYPES(cls): + # No inputs required. + return {"required": {}} + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("date", "time", "datetime", "datetime_for_filename") + FUNCTION = "generate_datetime" + CATEGORY = "Utility" + + def generate_datetime(self): + # Get the current datetime. + dt = datetime.now() + + # Format the date as "February 10, 2025" (with the day inserted without a leading zero). + date_str = dt.strftime("%B {day}, %Y").format(day=dt.day) + + # Format the time as "1:15pm" (12-hour format with any leading zero removed and lowercase am/pm). + time_str = dt.strftime("%I:%M%p").lstrip("0").lower() + + # Format the full datetime as "2025-02-10 1:15:33pm". + datetime_str = dt.strftime("%Y-%m-%d ") + dt.strftime("%I:%M:%S%p").lstrip("0").lower() + + # Format the datetime for filenames as "2025-02-10_13-15-33". + datetime_filename_str = dt.strftime("%Y-%m-%d_%H-%M-%S") + + return (date_str, time_str, datetime_str, datetime_filename_str) + +class EbuPromptHelperConsumeListItem: + """ + EBU PromptHelper Consume List Item Node + + This node takes an optional prompt, a target substring to replace, and a list of options provided + as a newline-separated string. It randomly selects one option from the list (using the provided seed + for reproducibility), removes that option from the list, and replaces all occurrences of the target + substring in the prompt with the selected option. The revised list (with the selected option removed) + is also returned. + + Inputs: + - word_to_replace (STRING): The substring in the prompt to be replaced. + - list (STRING): A newline-separated list of options. + - seed (INT): A seed for the random number generator (0 means non-deterministic). + - prompt_text (STRING, optional): A multiline string representing the input prompt. Defaults to an empty string. + + Returns: + - updated_prompt_text (STRING): The prompt with the target substring replaced by the selected option. + - word_selected (STRING): The randomly selected option from the list. + - revised_list (STRING): The remaining options (after the selected one is removed) joined by a single newline + between each option, with no extra newline at the beginning. + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "word_to_replace": ("STRING", {}), + "list": ("STRING", {"multiline": True, "default": ""}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + "optional": { + "prompt_text": ("STRING", {"multiline": True, "default": ""}) + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING") + RETURN_NAMES = ("updated_prompt_text", "word_selected", "revised_list") + FUNCTION = "consume_list_item" + CATEGORY = "utils" + + def consume_list_item(self, word_to_replace, list, seed, prompt_text=""): + # Split the input string into a list of options using newlines. + options = [opt.strip() for opt in list.splitlines() if opt.strip()] + + # Set the random seed. + random.seed(seed) + + if options: + # Randomly select an option. + word_selected = random.choice(options) + + # Remove the selected option from the list. + options.remove(word_selected) + + # Join the remaining options with a single newline between them. + revised_list = '\n'.join(options) + # Ensure there is no extra newline at the beginning. + revised_list = revised_list.lstrip('\n') + + # Replace all occurrences of the target substring in the prompt with the selected option. + updated_prompt_text = prompt_text.replace(word_to_replace, word_selected) + return (updated_prompt_text, word_selected, revised_list) + else: + # If no options are provided, return the prompt (or empty string) and empty values for the other outputs. + return (prompt_text, "", "") + +class EbuPromptHelperSeasonWeatherTimeOfDay: + """ + EBU PromptHelper Season Weather Time-Of-Day Node + + This node generates a random abstract description of a datetime within a specified year and time range, + and then produces a weather description based on that abstract datetime. The abstract datetime is + expressed in terms of a time of day and a seasonal descriptor (for example, "early morning during early winter of 2020"). + + Two new parameters allow you to skew the randomness: + - year_skew: How to bias the year selection. + - time_of_day_skew: How to bias the time selection. + + The available options for each are: + - "no skew" (default): Uniform random selection. + - "earlier of two": Generate two random values and pick the earlier (lower) one. + - "later of two": Generate two random values and pick the later (higher) one. + - "middle of three": Generate three random values and pick the median. + + Inputs: + - year_from (INT): The earliest year for the random date (e.g., 1980). + - year_to (INT): The latest year for the random date (e.g., 2022). + - time_from (STRING): The start of the time range (e.g., "6:00am"). + - time_to (STRING): The end of the time range (e.g., "7:00pm"). + - seed (INT): to ensure it runs every time + - year_skew (STRING): Dropdown with options ["no skew", "earlier of two", "later of two", "middle of three"]. + - time_of_day_skew (STRING): Dropdown with the same options as above. + + Returns: + - when (STRING): An abstract datetime description (e.g., "early morning during early winter of 2020"). + - when_no_year (STRING): The abstract datetime description with the year removed (e.g., "early morning during early winter"). + - weather (STRING): A weather description (e.g., "cool and snowy") selected based on the abstract datetime. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "year_from": ("INT", {"default": 1980, "step": 1, "display": "number"}), + "year_to": ("INT", {"default": 2025, "step": 1, "display": "number"}), + "time_from": ("STRING", {"multiline": False, "default": "6:00am"}), + "time_to": ("STRING", {"multiline": False, "default": "7:00pm"}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "year_skew": (["no skew", "earlier of two", "later of two", "middle of three"], {"default": "no skew"}), + "time_of_day_skew": (["no skew", "earlier of two", "later of two", "middle of three"], {"default": "no skew"}), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING") + RETURN_NAMES = ("when", "when_no_year", "weather") + FUNCTION = "generate_info" + CATEGORY = "Example" + + def generate_info(self, year_from, year_to, time_from, time_to, seed, year_skew, time_of_day_skew): + random.seed(seed) + when = self.generate_random_datetime(year_from, year_to, time_from, time_to, year_skew, time_of_day_skew) + weather = generate_weather_description(when) # Use external weather function + # Remove the " of [year]" part from the abstract datetime. + when_no_year = when.rsplit(" of ", 1)[0] + return (when, when_no_year, weather) + + def generate_random_datetime(self, year_from, year_to, time_from, time_to, year_skew, time_of_day_skew): + # Helper: Convert a time string (e.g., "6:00am") to a datetime object. + def time_str_to_dt(time_str): + return datetime.strptime(time_str, '%I:%M%p') + + # Helper: Generate a skewed random integer between min_val and max_val (inclusive). + def generate_skewed_random(min_val, max_val, skew): + if skew == "no skew": + return random.randint(min_val, max_val) + elif skew == "earlier of two": + return min(random.randint(min_val, max_val), random.randint(min_val, max_val)) + elif skew == "later of two": + return max(random.randint(min_val, max_val), random.randint(min_val, max_val)) + elif skew == "middle of three": + vals = [random.randint(min_val, max_val) for _ in range(3)] + return sorted(vals)[1] + else: + return random.randint(min_val, max_val) + + # Helper: Generate a random date between start_year and end_year using the specified skew. + def random_date(start_year, end_year, skew): + start_date = datetime(start_year, 1, 1) + end_date = datetime(end_year + 1, 1, 1) + delta_days = (end_date - start_date).days + day_offset = generate_skewed_random(0, delta_days - 1, skew) + return start_date + timedelta(days=day_offset) + + # Helper: Determine the abstract time of day based on the hour. + def get_time_of_day(hour): + if 5 <= hour < 8: + return "early morning" + elif 8 <= hour < 10: + return "morning" + elif 10 <= hour < 12: + return "late morning" + elif 12 <= hour < 15: + return "early afternoon" + elif 15 <= hour < 18: + return "late afternoon" + elif 18 <= hour < 20: + return "early evening" + elif 20 <= hour < 22: + return "evening" + elif 22 <= hour or hour < 5: + return "night" + else: + return "unknown time" + + # Helper: Determine the seasonal descriptor based on the month. + def get_season_part(month): + if month in [12, 1, 2]: + season = "winter" + elif month in [3, 4, 5]: + season = "spring" + elif month in [6, 7, 8]: + season = "summer" + elif month in [9, 10, 11]: + season = "fall" + else: + season = "unknown season" + if month in [12, 1, 2]: + return f"early {season}" if month == 12 else f"late {season}" + elif month in [3, 4, 5]: + if month == 3: + return f"early {season}" + elif month == 5: + return f"late {season}" + else: + return f"middle of the {season}" + elif month in [6, 7, 8]: + if month == 6: + return f"early {season}" + elif month == 8: + return f"late {season}" + else: + return f"middle of the {season}" + elif month in [9, 10, 11]: + if month == 9: + return f"early {season}" + elif month == 11: + return f"late {season}" + else: + return f"middle of the {season}" + + # Convert time strings to datetime objects. + time_from_dt = time_str_to_dt(time_from) + time_to_dt = time_str_to_dt(time_to) + if time_from_dt > time_to_dt: + raise ValueError("time_from must be before time_to") + + # Generate a random date using the year skew. + random_date_obj = random_date(year_from, year_to, year_skew) + + # Calculate the total seconds in the time window and generate a skewed random second offset. + total_seconds = int((time_to_dt - time_from_dt).total_seconds()) + sec_offset = generate_skewed_random(0, total_seconds, time_of_day_skew) + random_time = time_from_dt + timedelta(seconds=sec_offset) + + # Determine the descriptive strings. + tod_str = get_time_of_day(random_time.hour) + season_str = get_season_part(random_date_obj.month) + + return f"{tod_str} during {season_str} of {random_date_obj.year}" + +# Node registration mappings. +NODE_CLASS_MAPPINGS = { + "EbuPromptHelperCombineTwoStrings": EbuPromptHelperCombineTwoStrings, + "EbuPromptHelperConsumeListItem": EbuPromptHelperConsumeListItem, + "EbuPromptHelperCurrentDateTime": EbuPromptHelperCurrentDateTime, + "EbuPromptHelperListSampler": EbuPromptHelperListSampler, + "EbuPromptHelperLoadFileAsString": EbuPromptHelperLoadFileAsString, + "EbuPromptHelperRandomColorPalette": EbuPromptHelperRandomColorPalette, + "EbuPromptHelperRandomize": EbuPromptHelperRandomize, + "EbuPromptHelperReplace": EbuPromptHelperReplace, + "EbuPromptHelperSeasonWeatherTimeOfDay": EbuPromptHelperSeasonWeatherTimeOfDay, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "EbuPromptHelperCombineTwoStrings": "EBU PromptHelper Combine Two Strings", + "EbuPromptHelperConsumeListItem": "EBU PromptHelper Consume List Item", + "EbuPromptHelperCurrentDateTime": "EBU PromptHelper Current DateTime", + "EbuPromptHelperListSampler": "EBU PromptHelper List Sampler", + "EbuPromptHelperLoadFileAsString": "EBU PromptHelper Load File as String", + "EbuPromptHelperRandomColorPalette": "EBU PromptHelper Color Palette", + "EbuPromptHelperRandomize": "EBU PromptHelper Randomize", + "EbuPromptHelperReplace": "EBU PromptHelper Replace", + "EbuPromptHelperSeasonWeatherTimeOfDay": "EBU PromptHelper Season Weather Time-Of-Day", +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4c1db10 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "ComfyUI_EBU_PromptHelper" +description = "This ComfyUI extension provides a suite of custom nodes for enhancing and manipulating prompts. Features include random color palette generation, prompt text replacement, prompt randomization using variable names and lists of options, list sampling, file loading, current datetime formatting, and season/weather/time-of-day generation." +version = "1.0.0" +authors = [ + {name = "burnsbert"} +] +keywords = keywords = ["comfyui", "prompt", "random", "color", "weather", "datetime", "utf8", "utility"] +license = {file = "LICENSE"} + +[project.urls] +Repository = "https://github.com/burnsbert/ComfyUI-EBU-PromptHelper" + +[tool.comfy] +PublisherId = "burnsbert" +DisplayName = "ComfyUI-EBU-PromptHelper" \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..27f31f2 --- /dev/null +++ b/readme.md @@ -0,0 +1,194 @@ +# ComfyUI-EBU-PromptHelper + +A collection of custom nodes for ComfyUI designed to enhance and manipulate prompts for image generation. These can be very useful for constructing workflows that randomize a prompt along a theme to have a lot of variety from generation to generation without having to hand generate prompts. + +These nodes provide a suite of utilities for generating random color palettes, modifying and randomizing text, sampling lists, loading file contents, formatting datetime strings, and even creating abstract weather descriptions along with a randomly generated year, season, and time of day. + +These work great with LLM custom nodes like my ComfyUI_EBU_LMStudio package. + +See the included workflow `flux_example_ebu_prompthelper.json` for usage examples. + +## Features + +- **Random Color Palette Generation:** + Generate diverse color palettes from multiple types (analogous, art house, chaotic, complementary, compound, split complementary, tetradic, triadic) with options to prefer or avoid certain color families. Color families can be customized via `color_data.py`. + +- **Prompt Text Replacement:** + Replace targeted substrings in prompts using customizable patterns. Multiple words can be specified with a delimiter (|). + +- **Prompt Randomization:** + Randomly replace substrings in a prompt with options from a list. The list can be delimited by newlines, commas, or semi-colons. + +- **String Combination:** + Combine two strings using a custom join string, with empty strings handled gracefully. + +- **List Sampling and Consumption:** + Randomly sample items from a multi-line list with optional numbering, plus functionality to consume list items one at a time. + +- **File Loading:** + Load the contents of a file as a string using UTF-8 encoding for easy prompt import and template management. + +- **Current Date/Time Generation:** + Retrieve the current datetime in multiple formats, useful for timestamping or dynamic prompt generation. + +- **Season, Weather, and Time-of-Day Generation:** + Generate abstract datetime and weather descriptions with advanced options for skewing the random selection of both year and time-of-day. + +## Installation + +1. **Copy Files:** + Place the entire `ComfyUI-EBU-PromptHelper` directory into your `ComfyUI/custom_nodes/` directory. + +2. **Dependencies:** + - Ensure you have Python 3.11 or newer + - Make sure ComfyUI is installed and updated + +3. **Restart ComfyUI:** + After copying the files and verifying your dependencies, restart ComfyUI to load the custom nodes. + +## Nodes + +### EBU PromptHelper Random Color Palette + +Generates random color palettes using several possible generation methods. One method is randomly chosen from the enabled options. + +**Inputs:** +- `include_analogous_palettes` (BOOLEAN): Include analogous palettes +- `include_art_house_palettes` (BOOLEAN): Include art house palettes +- `include_chaotic_palettes` (BOOLEAN): Include chaotic palettes +- `include_complementary_palettes` (BOOLEAN): Include complementary palettes +- `include_compound_palettes` (BOOLEAN): Include compound palettes +- `include_split_complementary_palettes` (BOOLEAN): Include split complementary palettes +- `include_tetradic_palettes` (BOOLEAN): Include tetradic palettes +- `include_triadic_palettes` (BOOLEAN): Include triadic palettes +- `palette_size` (STRING): "3 colors", "4 colors", or "5 colors" +- `prefer_color_family` (STRING): e.g., "None", "Reds", "Blues", etc. +- `avoid_color_family` (STRING): e.g., "None", "Neutrals", etc. +- `seed` (INT): Random seed (0 for non-deterministic) + +**Returns:** +- `palette_string` (STRING): Comma-separated color names +- `color1` ... `color5` (STRING): Individual colors +- `palette_type` (STRING): The palette method used +- `hex_values` (STRING): Comma-separated hex codes + +### EBU PromptHelper Replace + +Replaces occurrences of one or more target words in a prompt with a specified replacement string. + +**Inputs:** +- `prompt_text` (STRING): Input prompt text +- `word_to_replace` (STRING): Target word(s) to replace (use | as delimiter for multiple) +- `replace_with` (STRING): Replacement text +- `case_sensitive` (BOOLEAN): Whether to match case when replacing + +**Returns:** +- `updated_prompt_text` (STRING): The modified prompt + +### EBU PromptHelper Randomize + +Replaces a target substring with a randomly selected option from a list. + +**Inputs:** +- `prompt_text` (STRING): Input prompt text +- `word_to_replace` (STRING): Target substring to replace +- `replacement_options` (STRING): List of possible replacements +- `random_seed` (INT): Seed for random selection +- `case_sensitive` (BOOLEAN): Whether to match case when replacing +- `delimit_options_with` (STRING): How to split options - "newlines", "commas", or "semi-colons" + +**Returns:** +- `updated_prompt_text` (STRING): The modified prompt +- `word_selected` (STRING): The chosen replacement + +### EBU PromptHelper Combine Two Strings + +Combines two strings using a specified join string. + +**Inputs:** +- `str1` (STRING): First string +- `str2` (STRING): Second string +- `join_str` (STRING): String used to join (default: "\n\n") + +**Returns:** +- `combined` (STRING): The joined result + +### EBU PromptHelper List Sampler + +Samples random items from a list, with optional numbering. + +**Inputs:** +- `list` (STRING): Newline-separated list of items +- `seed` (INT): Random seed +- `number_of_elements` (INT, optional): Number of items to sample (default: 10) +- `number_sampled_list` (BOOLEAN): Whether to number the output list + +**Returns:** +- (STRING): Sampled items as a newline-separated string + +### EBU PromptHelper Load File as String + +Loads file contents as a string using UTF-8 encoding. + +**Inputs:** +- `directory` (STRING): File directory path +- `file_name` (STRING): Name of file to load + +**Returns:** +- (STRING): File contents or empty string on error + +### EBU PromptHelper Current DateTime + +Returns the current date/time in various formats. + +**Returns:** +- `date` (STRING): e.g., "February 10, 2025" +- `time` (STRING): e.g., "1:15pm" +- `datetime` (STRING): e.g., "2025-02-10 1:15:33pm" +- `datetime_for_filename` (STRING): e.g., "2025-02-10_13-15-33" + +### EBU PromptHelper Consume List Item + +Selects and removes an item from a list while updating a prompt. + +**Inputs:** +- `word_to_replace` (STRING): Target substring to replace +- `list` (STRING): Newline-separated list of options +- `seed` (INT): Random seed +- `prompt_text` (STRING, optional): Input prompt text + +**Returns:** +- `updated_prompt_text` (STRING): Modified prompt +- `word_selected` (STRING): Selected list item +- `revised_list` (STRING): Remaining list items + +### EBU PromptHelper Season Weather Time-Of-Day + +Generates random datetime and weather descriptions. + +**Inputs:** +- `year_from` (INT): Start year +- `year_to` (INT): End year +- `time_from` (STRING): Start time (e.g., "6:00am") +- `time_to` (STRING): End time (e.g., "7:00pm") +- `random_seed` (INT): Random seed +- `year_skew` (STRING): How to bias year selection +- `time_of_day_skew` (STRING): How to bias time selection + +**Returns:** +- `when` (STRING): Full datetime description (e.g., "early morning during early winter of 2020") +- `when_no_year` (STRING): Description without year (e.g., "early morning during early winter") +- `weather` (STRING): Weather description + +## Requirements +- ComfyUI +- Python 3.11 or newer + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Thank you to the ComfyUI team for all that you do! +- Thank you to the community making so many great custom nodes available, you've inspired me to try to do likewise. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8b0b57 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +# No additional requirements beyond Python standard library \ No newline at end of file diff --git a/weather_utils.py b/weather_utils.py new file mode 100644 index 0000000..2211b00 --- /dev/null +++ b/weather_utils.py @@ -0,0 +1,319 @@ +import random + +# Winter weather outcomes – cold, frost, and frequent snowfall. +winter_weather = { + "morning": [ + ("clear with a light dusting of frost and a brisk, dry chill", 5), + ("overcast with low gray clouds and a gentle, steady snowfall", 8), + ("partly cloudy with scattered flurries and a sharp, invigorating cold", 6), + ("cloudy with a light, steady snow shower and a crisp, biting chill", 7), + ("clear with a pronounced frost and a piercing, refreshing cold", 4), + ("mostly clear with subtle flurries and a clean, invigorating chill", 5), + ("partly cloudy with intermittent flurries and a quiet, renewing chill", 6), + ("overcast with a light snow shower and a steady, biting chill", 7), + ("clear with an invigorating chill and a sparse scattering of flurries", 4), + ("overcast with continuous light snowfall and a noticeably piercing chill", 8), + ("partly cloudy with subtle snow flurries and a crisp, awakening chill", 6), + ("cloudy with a delicate mix of light snow and a refreshing, piercing frost", 7), + ("overcast with heavy snowfall and strong winds, creating near-whiteout conditions", 5), + ("partly cloudy with icy patches and a biting wind that cuts through the air", 6), + ("cloudy with freezing rain and a slick, icy glaze covering everything", 4) + ], + "afternoon": [ + ("clear with a dominant blue sky and an intense, biting cold", 4), + ("partly cloudy with periodic bursts of snow flurries and gusty winds", 6), + ("overcast with continuous moderate snowfall and a relentless, piercing chill", 8), + ("cloudy with steady, light snow and a robust, unyielding cold", 7), + ("clear with a harsh, unyielding cold and sporadic flurries", 5), + ("overcast with persistent snow showers and a biting wind that intensifies the chill", 8), + ("partly cloudy with isolated snow showers and a severe, cutting cold", 6), + ("cloudy with a steady, drenching snow and gusts whipping the cold air", 7), + ("clear with an icy calm punctuated by erratic snow bursts and a severe, raw cold", 5), + ("overcast with relentless snowfall and powerful gusts that create a dramatically cold scene", 8), + ("partly cloudy with intermittent snow and a sharp, stinging cold", 6), + ("clear with a severe, dry cold and isolated, erratic flurries carried by biting winds", 5), + ("overcast with sleet and freezing rain, making roads and sidewalks treacherous", 7), + ("partly cloudy with a mix of snow and rain, creating slushy, messy conditions", 6) + ], + "evening": [ + ("partly cloudy with a soft drizzle and a gentle, fading chill leaving a delicate frost", 5), + ("overcast with intermittent light snow and a moderate, receding cold hinting at thaw", 6), + ("clear with a gradual easing of chill and sparse snowfall quietly settling", 4), + ("cloudy with a light, steady drizzle and a soft, diminishing chill", 5), + ("partly cloudy with a transient snow shower and a mild, soothing chill at dusk", 6), + ("overcast with a soft drizzle and a slow, calming decline in cold intensity", 7), + ("clear with a subtle easing of cold and occasional light flurries at twilight", 5), + ("cloudy with intermittent flurries and a gradually fading chill", 6), + ("partly cloudy with a light, intermittent drizzle that gently moderates the lingering cold", 5), + ("overcast with a brief, light snowfall and a mild, calming chill", 7), + ("clear with a delicate easing of cold and a trace of evaporating frost", 4), + ("partly cloudy with a transient drizzle and a modest, diminishing chill", 6), + ("overcast with freezing rain and a slick, icy coating on surfaces", 5) + ], + "night": [ + ("overcast with a heavy blanket of dark clouds, fierce winds, and a relentless, piercing cold", 7), + ("partly cloudy with scattered, shifting clouds and intermittent intense icy gusts", 6), + ("clear with a nearly cloudless expanse dominated by an overpowering, bone-chilling cold and shimmering frost", 5), + ("cloudy with a thick blanket of clouds and a severe, piercing cold", 7), + ("mostly overcast with turbulent winds and heavy snow creating a deeply penetrating, numbing cold", 8), + ("partly cloudy with isolated breaks revealing an icy, starlit sky and a sharp chill", 6), + ("overcast with a persistent, unyielding cold and a dense, gloomy cloud cover", 7), + ("clear with a sudden drop in temperature, punctuated by a few wispy clouds in the freezing night", 5), + ("cloudy with a full, dark sky and an intense, stinging cold that blankets the landscape in ice", 8), + ("partly cloudy with a sparse scattering of thin clouds and a penetrating, frigid chill", 6), + ("overcast with brooding, heavy clouds and a deep, numbing cold", 7), + ("clear with an almost cloudless, starry sky yet ruled by an uncompromising, bone-deep cold", 5), + ("overcast with freezing rain and a slick, icy glaze covering everything", 6) + ] +} + +# Spring weather outcomes – mild with moderate rainfall. +spring_weather = { + "morning": [ + ("mostly clear with a light drizzle and a refreshing, cool dampness that softens the air", 7), + ("clear with a soft mist and a gentle, awakening cool moisture", 8), + ("partly cloudy with intermittent showers and a crisp, invigorating coolness", 6), + ("overcast with a delicate shower and a subtle, enlivening moist air", 5), + ("mostly clear with a transient drizzle and a brisk, rejuvenating coolness", 7), + ("clear with a brief mist imparting soft, refreshing cool dampness", 8), + ("partly cloudy with a whisper of rain that gently imbues the morning with fresh moisture", 6), + ("overcast with steady light drizzle and a calm, refreshing cool moisture", 5), + ("mostly clear with a soft, fleeting drizzle that awakens gentle, crisp coolness", 7), + ("clear with intermittent light showers that lend subtle, invigorating cool dampness", 8), + ("partly cloudy with delicate raindrops that provide a quiet, refreshing cool air", 6), + ("overcast with a modest, persistent drizzle that softly revives the cool morning moisture", 5), + ("partly cloudy with a mix of sun and rain, creating a vibrant, refreshing atmosphere", 7), + ("overcast with steady rain and a cool, damp breeze", 6) + ], + "afternoon": [ + ("clear with intermittent moderate rain and a balanced, refreshing moistness", 7), + ("mostly clear with steady light showers and a vibrant, refreshing dampness", 8), + ("partly cloudy with regular rain bursts that infuse the air with a crisp, invigorating moisture", 6), + ("overcast with periodic rain bursts that evenly refresh the atmosphere", 5), + ("clear with rhythmic moderate showers that evenly rejuvenate the air", 7), + ("mostly clear with a sequence of light rain episodes sustaining a steady, cooling dampness", 8), + ("partly cloudy with consistent moderate rain that quietly energizes the air", 6), + ("clear with sporadic bursts of refreshing rain that subtly elevate the moist ambiance", 5), + ("mostly clear with a refreshing cadence of showers gently imbued with cool moisture", 7), + ("clear with periodic light showers steadily infusing the air with fresh, cool moisture", 8), + ("partly cloudy with intermittent rain providing a crisp, refreshing moist feel", 6), + ("overcast with recurring gentle showers consistently enlivening the moist atmosphere", 5), + ("partly cloudy with a mix of sun and rain, creating a vibrant, refreshing atmosphere", 7), + ("overcast with steady rain and a cool, damp breeze", 6) + ], + "evening": [ + ("partly cloudy with a soft drizzle and a gentle, receding cool moisture that eases dusk", 7), + ("overcast with intermittent light showers and a quiet, gradually fading moist air", 6), + ("clear with a brief rain and a mild, soothing moisture at dusk", 8), + ("cloudy with a light shower and a subtle, diminishing cool dampness", 7), + ("mostly clear with a transient rain gradually yielding gentle, soothing cool moistness", 8), + ("partly cloudy with quiet, intermittent drizzle softly diminishing the residual warmth", 6), + ("overcast with a brief, tender shower that smoothly transitions into delicate, receding moisture", 7), + ("clear with a fleeting rain gently subsiding into soft, refreshing moistness", 8), + ("partly cloudy with soft, scattered showers slowly easing the fading heat with gentle moisture", 6), + ("overcast with a modest drizzle gradually mellowing into soft, quiet cool dampness", 7), + ("clear with a subtle drizzle gently retreating, leaving delicate, refreshing cool moisture", 8), + ("partly cloudy with a light, sporadic drizzle gradually diminishing the evening’s moist air", 6), + ("overcast with steady rain and a cool, damp breeze", 7) + ], + "night": [ + ("overcast with steady, light rain and a cool, persistent dampness blanketing the night", 7), + ("cloudy with continuous drizzle and a lingering, cool moisture softly pervading the dark", 8), + ("mostly overcast with gentle, ongoing rain and a calm, cool damp air settling quietly", 6), + ("overcast with consistent drizzle and a steady, refreshing cool moisture that endures", 7), + ("overcast with a quiet, unwavering drizzle sustaining a cool, mellow dampness", 8), + ("cloudy with soft, continuous rain gently upholding a cool, enduring moisture", 6), + ("mostly overcast with subtle, persistent drizzle preserving a cool, refreshing dampness", 7), + ("overcast with steady, soft rain gently maintaining a calm, refreshing cool moisture", 8), + ("cloudy with restrained, ongoing drizzle subtly preserving the cool dampness", 6), + ("mostly overcast with low-key, continuous rain quietly refreshing the cool night", 7), + ("overcast with gentle, persistent showers delicately maintaining a cool, soft moisture", 8), + ("cloudy with a faint, ongoing drizzle softly holding refreshing cool dampness", 6), + ("overcast with steady rain and a cool, damp breeze", 7) + ] +} + +# Summer weather outcomes – warm and humid with occasional thunderstorms. +summer_weather = { + "morning": [ + ("mostly clear with sparse high clouds and a refreshing, cool breeze gently awakening the day", 8), + ("clear with a light scattering of wispy clouds and a mild, soothing coolness invigorating the morning", 9), + ("partly cloudy with a few thin, drifting clouds and a crisp, refreshing coolness setting a lively tone", 7), + ("overcast with diffused light and a gentle, moderate coolness offering brief respite", 5), + ("mostly clear with intermittent wisps of cloud providing subtle, cooling modulation to the crisp air", 7), + ("clear with an almost pristine sky punctuated by delicate, transient clouds lightly cooling the morning", 9), + ("partly cloudy with a faint layer of high clouds subtly tempering the early brightness", 7), + ("overcast with soft, diffuse cloud cover gently moderating a refreshing, cool start", 5), + ("mostly clear with a gentle interplay of light clouds imbuing the morning with invigorating coolness", 7), + ("clear with a modest scattering of high clouds enhancing a refreshing, clean coolness", 9), + ("partly cloudy with a delicate mist of clouds softly augmenting the refreshing cool ambiance", 7), + ("overcast with an airy cloud cover subtly infusing the morning with a soothing cool breeze", 5) + ], + "afternoon": [ + ("clear with a vivid blue sky and an intense, searing heat dominating the scene", 5), + ("mostly clear with an almost unbroken blue expanse and a blazing, relentless sun scorching the day", 5), + ("a brilliant blue sky with strong, direct sunlight and overpowering, searing heat", 5), + ("overcast with a thin, wispy layer of clouds barely softening the severe, unyielding heat", 4), + ("clear with a stark blue expanse intensifying the extreme, burning heat of the afternoon", 5), + ("mostly clear with sporadic delicate clouds offering little relief from oppressive, sweltering heat", 4), + ("clear with an unclouded sky magnifying the fierce, unrelenting sun and its scorching heat", 5), + ("mostly clear with a pristine blue dome failing to mitigate the overwhelming, intense heat", 5), + ("clear with a dominant blue sky accentuating the harsh, persistent, scorching warmth", 5), + ("mostly clear with an expansive, vivid blue underscoring the searing, unremitting heat", 5), + ("clear with a pure blue canopy amplifying the relentless, burning heat", 5), + ("mostly clear with an immaculate blue sky and unyielding, intense burning heat", 5), + ("partly cloudy with building cumulus clouds and a chance of scattered thunderstorms later in the day", 6), + ("overcast with thick, gray clouds and a humid, oppressive feel, signaling an approaching thunderstorm", 7), + ("partly cloudy with occasional rain showers and a muggy, sticky atmosphere", 6), + ("overcast with steady, light rain and a cool, refreshing break from the heat", 5), + ("partly cloudy with sudden, intense thunderstorms and gusty winds, followed by a cooler breeze", 6), + ("overcast with a steady drizzle and a noticeable drop in temperature, offering relief from the heat", 5), + ("partly cloudy with isolated thunderstorms and a mix of sun and rain throughout the afternoon", 6), + ("overcast with heavy rain and occasional thunder, creating a dramatic, cooling effect", 5) + ], + "evening": [ + ("partly cloudy with shifting clouds intermittently easing the residual warmth as dusk approaches", 7), + ("mostly sunny with occasional, fleeting clouds offering brief relief from lingering heat", 7), + ("a warm evening with a dynamic mix of sun and transient clouds gently cooling the air", 7), + ("overcast with a gradual increase in cloud cover softly moderating the fading heat", 6), + ("partly cloudy with ephemeral cloud patches providing intermittent cooling in the afterglow", 7), + ("mostly sunny with transient clouds offering sporadic relief from persistent warmth", 7), + ("partly cloudy with gentle modulation of sun and clouds slowly easing the residual heat", 7), + ("overcast with soft, gradually building cloud cover steadily cooling the lingering day", 6), + ("a warm evening with modest cloud cover softly cooling the air", 7), + ("mostly sunny with ephemeral cloud formations briefly easing the afterglow", 7), + ("partly cloudy with gradual softening of sunlight as intermittent clouds bring cooling relief", 7), + ("overcast with steady, modest cloud cover gradually diminishing the remaining warmth", 6) + ], + "night": [ + ("clear with a refreshing drop in temperature and an almost cloudless, starry sky that cools rapidly", 8), + ("mostly clear with a delicate scattering of high clouds providing subtle intermittent relief from residual heat", 7), + ("partly cloudy with intermittent patches of cloud gently moderating lingering warmth to yield a cool night", 7), + ("overcast with soft, uniform cloud cover significantly cooling the atmosphere as night falls", 6), + ("clear with an abrupt cooling revealing a sparse scattering of wispy clouds under a starry sky", 8), + ("mostly cloudy with dynamic cloud cover intermittently breaking up to offer refreshing, cool relief", 7), + ("partly cloudy with varied shifting clouds intermittently softening the intense coolness", 7), + ("overcast with continuous gentle cloud cover transforming the night into a cool, subdued scene", 6), + ("clear with a marked cooling and occasional fleeting clouds subtly enhancing the coolness", 8), + ("mostly clear with a light, ever-changing cloud pattern softly supporting the cooling night", 7), + ("partly cloudy with delicate shifting clouds intermittently providing relief from the crisp, cool night", 7), + ("overcast with persistent soft cloud layer maintaining consistent, refreshing coolness throughout the night", 6) + ] +} + +# Fall weather outcomes – cool with variable precipitation. +fall_weather = { + "morning": [ + ("mostly clear with a subtle hint of overcast and a crisp, cool air that feels dry", 8), + ("clear with minimal cloud cover and a refreshing, cool, dry atmosphere that awakens the senses", 9), + ("partly cloudy with a modest scattering of clouds and a distinct, cool dryness that energizes the morning", 8), + ("overcast with a gentle cloud layer and a refreshing, dry coolness that softens the early light", 7), + ("mostly clear with a fleeting overcast subtly enhancing the dry, crisp coolness", 8), + ("clear with a delicate accumulation of clouds and a bright, invigorating dry coolness", 9), + ("partly cloudy with intermittent clouds creating a balanced dry cool atmosphere", 8), + ("overcast with soft, diffused clouds delivering a steady, crisp, dry coolness", 7), + ("mostly clear with very light cloud presence accentuating refreshing, dry cool clarity", 8), + ("clear with subtle overcast gently underscoring vivid, dry coolness in the morning", 9), + ("partly cloudy with a gentle interplay of clouds softly supporting a bright, dry cool air", 8), + ("overcast with a light, ephemeral cloud cover quietly enhancing a crisp, dry coolness", 7), + ("partly cloudy with a mix of sun and rain, creating a vibrant, refreshing atmosphere", 7), + ("overcast with steady rain and a cool, damp breeze", 6) + ], + "afternoon": [ + ("clear with a modest rise in cloud cover and pronounced dry coolness pervading the air", 8), + ("mostly clear with a gentle increase in clouds and noticeably refreshing, dry cool atmosphere", 9), + ("partly cloudy with emerging clouds offering a defined dry cool feel", 8), + ("overcast with subtle, persistent clouds reinforcing steady, dry coolness", 7), + ("clear with a gentle scattering of clouds modestly sustaining a vivid, dry cool environment", 8), + ("mostly clear with sporadic clouds intermittently boosting distinct dry cool air", 9), + ("partly cloudy with rhythmic cloud interplay gently supporting refreshing, dry coolness", 8), + ("overcast with soft, intermittent cloud cover sustaining a defined dry cool ambiance", 7), + ("clear with a barely perceptible cloud rise accentuating crisp, dry cool atmosphere", 8), + ("mostly clear with occasional cloud drifts subtly maintaining pronounced dry cool tone", 9), + ("partly cloudy with a steady cloud pattern reinforcing clear, dry cool atmosphere", 8), + ("overcast with light, rhythmic cloud cover quietly upholding steady, refreshing dry coolness", 7), + ("partly cloudy with a mix of sun and rain, creating a vibrant, refreshing atmosphere", 7), + ("overcast with steady rain and a cool, damp breeze", 6) + ], + "evening": [ + ("mostly clear with a gentle overcast and soft, fading dry coolness easing the day", 8), + ("clear with subtle cloud rise and mild, receding dry coolness softening the twilight", 9), + ("partly cloudy with transient overcast and delicate, diminishing dry coolness", 8), + ("overcast with faint, continuous clouds gently moderating residual dry cool air", 7), + ("partly cloudy with intermittent clouds gradually reducing lingering dry coolness", 8), + ("clear with soft, ephemeral overcast slowly diminishing residual dry cool feel", 9), + ("partly cloudy with modest cloud buildup softly alleviating fading dry cool atmosphere", 8), + ("overcast with steady, gentle clouds quietly tempering evening dry coolness", 7), + ("mostly clear with delicate sun-cloud interplay gradually easing dry cool air", 8), + ("clear with gradual cloud emergence softly reducing remaining dry coolness", 9), + ("partly cloudy with subtle cloud modulation gently lessening dry cool tone", 8), + ("overcast with measured cloud return quietly tempering lingering dry coolness", 7), + ("partly cloudy with a mix of sun and rain, creating a vibrant, refreshing atmosphere", 7), + ("overcast with steady rain and a cool, damp breeze", 6) + ], + "night": [ + ("clear with an abrupt cooling revealing a nearly cloudless, starry sky and a strong, biting dry chill", 8), + ("mostly clear with dynamic, shifting clouds intermittently breaking intense dry cold", 9), + ("partly cloudy with occasional fleeting clouds gently moderating severe, piercing dry chill", 8), + ("overcast with a thick, persistent cloud layer taming the intensity of biting dry cold", 7), + ("clear with noticeable cooling and scattered delicate clouds accentuating profound dry chill", 8), + ("mostly cloudy with robust, evolving clouds intermittently relieving harsh dry cold", 9), + ("partly cloudy with a varied mix of transient clouds subtly easing unyielding dry chill", 8), + ("overcast with dense, enveloping clouds gently diminishing raw, intense dry cold", 7), + ("clear with marked cooling and occasional delicate clouds enhancing piercing dry chill", 8), + ("mostly clear with slight, shifting cloud patterns softly supporting deep, biting dry cold", 9), + ("partly cloudy with dynamic cloud formations intermittently alleviating harsh, sharp dry chill", 8), + ("overcast with a dense, constant cloud cover quietly tempering overwhelming, burning dry cold", 7), + ("partly cloudy with a mix of sun and rain, creating a vibrant, refreshing atmosphere", 7), + ("overcast with steady rain and a cool, damp breeze", 6) + ] +} + +def generate_weather_description(abstract_datetime_str): + """ + Generates a detailed, singular weather description based on an abstract datetime string. + The abstract_datetime_str should include keywords indicating the season ("winter", "spring", + "summer", "fall") and a time-of-day keyword ("morning", "afternoon", "evening", or "night"). + This function selects the corresponding weather outcomes from the appropriate season's dictionary + using weighted random selection, and returns a random atmospheric description that focuses on cloud cover, + precipitation, moisture, and overall conditions. + Returns: + A weather description string. + """ + # Determine the season. + if "winter" in abstract_datetime_str: + season = "winter" + elif "spring" in abstract_datetime_str: + season = "spring" + elif "summer" in abstract_datetime_str: + season = "summer" + elif "fall" in abstract_datetime_str: + season = "fall" + else: + return "unpredictable weather" + # Determine the time of day. + if "morning" in abstract_datetime_str: + tod = "morning" + elif "afternoon" in abstract_datetime_str: + tod = "afternoon" + elif "evening" in abstract_datetime_str or "dusk" in abstract_datetime_str: + tod = "evening" + elif "night" in abstract_datetime_str: + tod = "night" + else: + tod = "morning" # Default to morning if not found. + # Select the appropriate weather options. + if season == "winter": + options = winter_weather[tod] + elif season == "spring": + options = spring_weather[tod] + elif season == "summer": + options = summer_weather[tod] + elif season == "fall": + options = fall_weather[tod] + else: + options = [("unpredictable weather", 1)] + # Use weighted random selection. + descriptions = [opt[0] for opt in options] + weights = [opt[1] for opt in options] + return random.choices(descriptions, weights=weights, k=1)[0]