├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── __init__.py ├── node.py └── pyproject.toml /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Publish Custom Node 18 | uses: Comfy-Org/publish-node-action@main 19 | with: 20 | ## Add your own personal access token to your Github Repository secrets and reference it here. 21 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "prompt-lists"] 2 | path = prompt-lists 3 | url = https://github.com/ai-prompts/prompt-lists.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 fofr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-Prompter-fofrAI 2 | 3 | A prompt helper. Use templates from https://prompter.fofr.ai/ in ComfyUI. 4 | 5 | https://github.com/fofr/ComfyUI-Prompter-fofrAI/assets/319055/fa5d40ea-5fe9-4d98-94bc-e91b5e66edd0 6 | 7 | For example: 8 | 9 | > A film still of [character.fantasy], [interaction.couple], [cinematic.keyword], [cinematic.coloring], [cinematic.effect], set in [time.year] 10 | 11 | Gives: 12 | 13 | > A film still of a ghost hunter, conflict, 8mm film, low-key color grading, cineon, set in 1890 14 | 15 | > A film still of a siren, holding a grudge, prores, hdr, haze, set in 1909 16 | 17 | > A film still of a demon princess, assistance, 8k resolution, hard light, bokeh, set in 1858 18 | 19 | ## Nodes 20 | 21 | - Prompt from template: Turn a template into a prompt 22 | - List sampler: Sample items from a list, sequentially or randomly 23 | 24 | ## Prompt template features 25 | 26 | Multiple list items: 27 | 28 | [animal.mammal,2] 29 | 30 | > giraffe, lion 31 | 32 | Get multiple items from multiple random lists: 33 | 34 | [random,2] 35 | 36 | > demon princess, hdr 37 | 38 | ## Lists 39 | 40 | You can use the list sampler node to see what prompt lists are available. 41 | 42 | See available lists: 43 | 44 | https://github.com/ai-prompts/prompt-lists/blob/main/list-metadata.json 45 | 46 | Explore lists here: 47 | 48 | https://prompter.fofr.ai/explore 49 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .node import NODE_CLASS_MAPPINGS 2 | __all__ = ['NODE_CLASS_MAPPINGS'] 3 | -------------------------------------------------------------------------------- /node.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import os 4 | import random 5 | 6 | 7 | class BasePrompt: 8 | PROMPT_LISTS_DIR = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), "prompt-lists" 10 | ) 11 | 12 | def __init__(self): 13 | self.all_lists = {} 14 | for list_name in self.get_all_available_lists(): 15 | self.all_lists[list_name] = self.load_list(list_name) 16 | 17 | @classmethod 18 | def get_all_available_lists(cls): 19 | lists_path = os.path.join(cls.PROMPT_LISTS_DIR, "lists.json") 20 | with open(lists_path, "r", encoding="utf-8") as file: 21 | lists = json.load(file) 22 | return lists 23 | 24 | def load_list(self, list_name): 25 | hyphenated_list = re.sub(r"([a-z])([A-Z])", r"\1-\2", list_name).lower() 26 | category, name = hyphenated_list.split(".") 27 | lists_path = os.path.join(self.PROMPT_LISTS_DIR, f"lists/{category}/{name}.yml") 28 | with open(lists_path, "r", encoding="utf-8") as file: 29 | content = file.read() 30 | list_data = content.split("---")[2].strip().split("\n") 31 | 32 | return list_data 33 | 34 | 35 | class PromptFromTemplate(BasePrompt): 36 | @classmethod 37 | def INPUT_TYPES(cls): 38 | return { 39 | "required": { 40 | "template": ( 41 | "STRING", 42 | {"default": "", "multiline": True, "dynamicPrompts": True}, 43 | ), 44 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}), 45 | } 46 | } 47 | 48 | RETURN_TYPES = ("STRING",) 49 | FUNCTION = "generate_prompt_from_template" 50 | CATEGORY = "Prompter" 51 | 52 | def generate_prompt_from_template(self, template, seed=0): 53 | random.seed(seed) 54 | 55 | def replace_match(match): 56 | list_name = match.group(1) 57 | list_name_parts = list_name.split(",") 58 | list_name = list_name_parts[0].strip() 59 | item_count = int(list_name_parts[1]) if len(list_name_parts) == 2 else 1 60 | item_count = min(item_count, 50) 61 | 62 | if list_name == "random": 63 | random_list_names = [self.get_random_list() for _ in range(item_count)] 64 | random_items = [ 65 | self.get_random_items_from_list(list_name, 1)[0] 66 | for list_name in random_list_names 67 | ] 68 | return ", ".join(random_items) 69 | 70 | if list_name not in self.all_lists: 71 | return f"[{list_name}]" 72 | 73 | return ", ".join(self.get_random_items_from_list(list_name, item_count)) 74 | 75 | prompt = re.sub(r"\[(.*?)\]", replace_match, template) 76 | print(prompt) 77 | return (prompt,) 78 | 79 | def get_random_list(self): 80 | return random.choice(list(self.all_lists.keys())) 81 | 82 | def get_random_items_from_list(self, list_name, item_count): 83 | return random.sample(self.all_lists[list_name], item_count) 84 | 85 | 86 | class PromptListSampler(BasePrompt): 87 | @classmethod 88 | def INPUT_TYPES(cls): 89 | return { 90 | "required": { 91 | "list_name": (cls.get_all_available_lists(),), 92 | "mode": (["random", "sequential"], {"default": "sequential"}), 93 | "number_of_items": ("INT", {"default": 1, "min": 1}), 94 | "join_with": ("STRING", {"default": ", "}), 95 | "index": ( 96 | "INT", 97 | { 98 | "default": 0, 99 | "min": 0, 100 | "max": 0xFFFFFFFFFFFFFFFF, 101 | "control_after_generate": True, 102 | }, 103 | ), 104 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}), 105 | } 106 | } 107 | 108 | RETURN_TYPES = ("STRING",) 109 | FUNCTION = "get_item_from_list" 110 | CATEGORY = "Prompter" 111 | 112 | def get_item_from_list( 113 | self, 114 | list_name, 115 | index=0, 116 | number_of_items=1, 117 | mode="sequential", 118 | join_with=", ", 119 | seed=0, 120 | ): 121 | random.seed(seed) 122 | 123 | if list_name not in self.all_lists: 124 | return f"[{list_name}]" 125 | 126 | if mode == "random": 127 | items = random.sample( 128 | self.all_lists[list_name], 129 | min(number_of_items, len(self.all_lists[list_name])), 130 | ) 131 | else: 132 | index = index % len(self.all_lists[list_name]) 133 | items = self.all_lists[list_name][index : index + number_of_items] 134 | 135 | return (join_with.join(items),) 136 | 137 | 138 | NODE_CLASS_MAPPINGS = { 139 | "Prompt from template 🪴": PromptFromTemplate, 140 | "List sampler 🪴": PromptListSampler, 141 | } 142 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-prompter-fofrai" 3 | description = "A prompt helper for ComfyUI, based on prompter.fofr.ai" 4 | version = "1.0.3" 5 | license = { file = "LICENSE" } 6 | 7 | [project.urls] 8 | Repository = "https://github.com/fofr/comfyui-prompter-fofrai" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "fofr" 13 | DisplayName = "ComfyUI-Prompter-fofrAI" 14 | Icon = "🪴" 15 | --------------------------------------------------------------------------------