From 52b24e72081d43fce814f550ac2c3bbdf1ae42fd Mon Sep 17 00:00:00 2001 From: Marco Date: Fri, 29 Aug 2025 23:22:19 -0300 Subject: [PATCH] Add CSV Prompt Loader node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New node that loads prompts from CSV files with dropdown selection. CSV format: name,prompt,negative_prompt Features dropdown selection of prompt names and outputs selected prompt and negative prompt. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 239 +++++++++++++++++++++++++++++++++++++++++++ __init__.py | 7 +- csv_prompt_loader.py | 100 ++++++++++++++++++ 3 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 csv_prompt_loader.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..554fc53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,239 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be added to the global gitignore or merged into this project gitignore. For a PyCharm +# project, it is recommended to add version control directories. +.idea/ + +# VS Code +.vscode/ +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.lnk + +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# ComfyUI specific +*.safetensors +*.ckpt +*.pt +*.pth +*.bin +models/ +checkpoints/ +temp/ +output/ +input/ + +# Test images and temporary files +test_images/ +temp_images/ +*.tmp +*.temp + +# Local configuration files +config.json +settings.json +user_config.py + +# Log files +*.log +logs/ + +# Backup files +*.bak +*.backup +*~ + +# Cache directories +cache/ +.cache/ + +# Node specific temporary files +node_temp/ +processing_temp/ \ No newline at end of file diff --git a/__init__.py b/__init__.py index f957b02..0db3354 100644 --- a/__init__.py +++ b/__init__.py @@ -26,6 +26,7 @@ from .gimp_nodes.cie_lch_noise_gegl_like import CIELChNoiseGEGLLike from .gimp_nodes.image_type_detector import ImageTypeDetector from .gimp_nodes.mean_curvature_blur_gegl_like import MeanCurvatureBlurGEGLLike from .gimp_nodes.rgb_noise_gegl_like import RGBNoiseGEGLLike +from .csv_prompt_loader import CSVPromptLoader NODE_CLASS_MAPPINGS = { "CustomCrop": CustomCropNode, @@ -46,7 +47,8 @@ NODE_CLASS_MAPPINGS = { "CIELChNoiseGEGLLike": CIELChNoiseGEGLLike, "ImageTypeDetector": ImageTypeDetector, "MeanCurvatureBlurGEGLLike": MeanCurvatureBlurGEGLLike, - "RGBNoiseGEGLLike": RGBNoiseGEGLLike + "RGBNoiseGEGLLike": RGBNoiseGEGLLike, + "CSVPromptLoader": CSVPromptLoader } NODE_DISPLAY_NAME_MAPPINGS = { @@ -67,7 +69,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "CIELChNoiseGEGLLike": "CIE LCH Noise (GEGL-like)", "ImageTypeDetector": "Image Type Detector", "MeanCurvatureBlurGEGLLike": "Mean Curvature Blur (GEGL-like)", - "RGBNoiseGEGLLike": "RGB Noise (GEGL-like)" + "RGBNoiseGEGLLike": "RGB Noise (GEGL-like)", + "CSVPromptLoader": "CSV Prompt Loader" } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file diff --git a/csv_prompt_loader.py b/csv_prompt_loader.py new file mode 100644 index 0000000..8fce21a --- /dev/null +++ b/csv_prompt_loader.py @@ -0,0 +1,100 @@ +import csv +import os +import torch + +class CSVPromptLoader: + """ + A custom node for ComfyUI that loads prompts from a CSV file with dropdown selection. + The CSV file should have columns: name, prompt, negative_prompt + """ + + csv_cache = {} # Class-level cache to share data between instances + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "csv_path": ("STRING", {"default": ""}), + "selected_name": (cls._get_names_from_cache, {"default": ""}), + }, + } + + @classmethod + def _get_names_from_cache(cls): + """Get available names from the cached CSV data""" + names = [] + for path_data in cls.csv_cache.values(): + names.extend(path_data.keys()) + if not names: + names = ["No CSV loaded"] + return names + + @classmethod + def _load_csv_data(cls, csv_path): + """Load CSV data and cache it""" + if not os.path.exists(csv_path): + return {} + + data = {} + try: + with open(csv_path, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + for row in reader: + if 'name' in row and 'prompt' in row and 'negative_prompt' in row: + data[row['name']] = { + 'prompt': row['prompt'], + 'negative_prompt': row['negative_prompt'] + } + except Exception as e: + print(f"Error loading CSV: {e}") + return {} + + # Cache the data + cls.csv_cache[csv_path] = data + return data + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("prompt", "negative_prompt") + FUNCTION = "load_prompt" + CATEGORY = "text/prompt" + TITLE = "CSV Prompt Loader" + + def load_prompt(self, csv_path, selected_name): + # Load CSV data if not cached + if csv_path not in self.csv_cache: + self._load_csv_data(csv_path) + + # Get cached data + csv_data = self.csv_cache.get(csv_path, {}) + + # If no data loaded, return empty strings + if not csv_data: + return ("", "") + + # If selected name exists, return its data + if selected_name in csv_data: + data = csv_data[selected_name] + return (data['prompt'], data['negative_prompt']) + + # Return the first entry as fallback + if csv_data: + first_name = list(csv_data.keys())[0] + first_data = csv_data[first_name] + return (first_data['prompt'], first_data['negative_prompt']) + + return ("", "") + + @classmethod + def IS_CHANGED(cls, csv_path, selected_name): + return f"{csv_path}_{selected_name}" + + @classmethod + def VALIDATE_INPUTS(cls, csv_path, selected_name): + if not csv_path: + return "CSV path cannot be empty" + if not os.path.exists(csv_path): + return "CSV file does not exist" + return True \ No newline at end of file