Compare commits
31
Commits
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[Bug] ..."
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature] ..."
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# ComfyUI Styles CSV Loader Extension
|
||||||
|
|
||||||
|
This is a Python extension/plugin for ComfyUI that loads style prompts from CSV files, primarily for migration from Automatic1111 Stable Diffusion WebUI.
|
||||||
|
|
||||||
|
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||||
|
|
||||||
|
## Working Effectively
|
||||||
|
|
||||||
|
### Repository Structure
|
||||||
|
- **`styles_csv_loader.py`** - Main extension code containing the StylesCSVLoader class
|
||||||
|
- **`__init__.py`** - Module initialization that imports the main class
|
||||||
|
- **`pyproject.toml`** - Project metadata for Comfy Registry publishing
|
||||||
|
- **`README.md`** - User documentation
|
||||||
|
- **`run_tests.py`** - Test runner script using pytest framework
|
||||||
|
- **`tests/`** - Comprehensive test suite with fixtures and unit tests
|
||||||
|
- **`.github/workflows/publish.yml`** - Automated publishing to Comfy Registry
|
||||||
|
|
||||||
|
### Prerequisites and Dependencies
|
||||||
|
- This extension requires ComfyUI to be installed and running
|
||||||
|
- No additional Python packages need to be installed beyond ComfyUI's dependencies
|
||||||
|
- The extension depends on ComfyUI's `folder_paths` module for file path resolution
|
||||||
|
- Python 3.6+ is required (follows ComfyUI requirements)
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
- **No build process required** - This is a pure Python plugin that loads directly into ComfyUI
|
||||||
|
- **Automated testing framework** - Uses pytest with comprehensive test coverage
|
||||||
|
- **No linting configuration** - Follow Python PEP 8 standards manually
|
||||||
|
- Always validate Python syntax after making changes: `python3 -c "import ast; ast.parse(open('styles_csv_loader.py').read())"`
|
||||||
|
- Always validate init file syntax: `python3 -c "import ast; ast.parse(open('__init__.py').read())"`
|
||||||
|
|
||||||
|
### Testing and Validation
|
||||||
|
- **Run the test suite**: Use the automated testing framework for comprehensive validation:
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
python run_tests.py
|
||||||
|
|
||||||
|
# Run tests with coverage reporting
|
||||||
|
python run_tests.py --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Test Structure**: The test suite includes:
|
||||||
|
- Unit tests for CSV parsing logic
|
||||||
|
- Tests for various CSV formats (valid, invalid, complex)
|
||||||
|
- Integration tests for ComfyUI node functionality
|
||||||
|
- Test fixtures with sample CSV files
|
||||||
|
|
||||||
|
- **Syntax Validation**: Always run before committing changes:
|
||||||
|
```bash
|
||||||
|
python3 -c "import ast; ast.parse(open('styles_csv_loader.py').read()); print('styles_csv_loader.py syntax valid')"
|
||||||
|
python3 -c "import ast; ast.parse(open('__init__.py').read()); print('__init__.py syntax valid')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSV File Format Requirements
|
||||||
|
- Must be named `styles.csv` and located in ComfyUI root directory (where `main.py` is)
|
||||||
|
- Format: `style_name,positive_prompt,negative_prompt`
|
||||||
|
- First row is header (ignored)
|
||||||
|
- Supports quoted fields with commas inside quotes
|
||||||
|
- Example valid CSV:
|
||||||
|
```csv
|
||||||
|
style_name,positive_prompt,negative_prompt
|
||||||
|
cinematic,cinematic lighting professional photography,low quality blurry
|
||||||
|
vintage,"old photo sepia tone, vintage style",modern digital
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Making Code Changes
|
||||||
|
1. Edit `styles_csv_loader.py` for core functionality changes
|
||||||
|
2. Run syntax validation: `python3 -c "import ast; ast.parse(open('styles_csv_loader.py').read())"`
|
||||||
|
3. Run the test suite to validate changes: `python run_tests.py`
|
||||||
|
4. If changing module structure, also validate `__init__.py`
|
||||||
|
|
||||||
|
### Adding New Features
|
||||||
|
- All new functionality should be added to the `StylesCSVLoader` class
|
||||||
|
- Follow ComfyUI node conventions:
|
||||||
|
- `INPUT_TYPES()` classmethod for defining inputs
|
||||||
|
- `RETURN_TYPES` and `RETURN_NAMES` class attributes
|
||||||
|
- `execute()` method for main functionality
|
||||||
|
- `FUNCTION` attribute pointing to execute method
|
||||||
|
- `CATEGORY` for node organization
|
||||||
|
|
||||||
|
### Debugging CSV Issues
|
||||||
|
- Run the test suite to identify issues: `python run_tests.py`
|
||||||
|
- Check test fixtures in `tests/fixtures/` for valid CSV examples
|
||||||
|
- Common issues:
|
||||||
|
- Unescaped quotes in CSV fields
|
||||||
|
- Wrong number of columns
|
||||||
|
- File encoding problems (use UTF-8)
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
- Update version in `pyproject.toml`
|
||||||
|
- Commit changes to main branch
|
||||||
|
- GitHub workflow automatically publishes to Comfy Registry
|
||||||
|
|
||||||
|
## File Contents Reference
|
||||||
|
|
||||||
|
### Repository Root Structure
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── .git/
|
||||||
|
├── .github/
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── publish.yml
|
||||||
|
├── .gitignore
|
||||||
|
├── LICENSE
|
||||||
|
├── README.md
|
||||||
|
├── __init__.py
|
||||||
|
├── pyproject.toml
|
||||||
|
├── run_tests.py
|
||||||
|
├── styles_csv_loader.py
|
||||||
|
└── tests/
|
||||||
|
├── __init__.py
|
||||||
|
├── fixtures/
|
||||||
|
│ ├── complex_styles.csv
|
||||||
|
│ ├── invalid_styles.csv
|
||||||
|
│ └── valid_styles.csv
|
||||||
|
└── test_styles_csv_loader.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Code Patterns
|
||||||
|
- CSV parsing regex: `,(?=(?:[^"]*"[^"]*")*[^"]*$)` - handles quoted fields with commas (matches literal double quotes)
|
||||||
|
- Error handling: Always return default error style on exceptions
|
||||||
|
- ComfyUI integration: Uses `folder_paths.base_path` for CSV location
|
||||||
|
- Node registration: `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS`
|
||||||
|
|
||||||
|
### Common Error Messages
|
||||||
|
- "Error loading styles.csv, check the console" - Default fallback style
|
||||||
|
- "No styles.csv found" - File missing from ComfyUI root
|
||||||
|
- CSV parsing errors - Usually due to malformed CSV format
|
||||||
|
|
||||||
|
## Validation Checklist
|
||||||
|
Before committing any changes:
|
||||||
|
- [ ] Run Python syntax validation on all modified .py files
|
||||||
|
- [ ] Run the test suite: `python run_tests.py`
|
||||||
|
- [ ] Verify no new dependencies introduced
|
||||||
|
- [ ] Update version in pyproject.toml if needed
|
||||||
|
- [ ] Ensure all tests pass
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: "Copilot Setup Steps"
|
||||||
|
|
||||||
|
# Automatically run the setup steps when they are changed to allow for easy validation, and
|
||||||
|
# allow manual testing through the repository's "Actions" tab
|
||||||
|
"on":
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
||||||
|
copilot-setup-steps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Set the permissions to the lowest permissions possible needed for your steps.
|
||||||
|
# Copilot will be given its own token for its operations.
|
||||||
|
permissions:
|
||||||
|
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
# You can define any steps you want, and they will run before the agent starts.
|
||||||
|
# If you do not check out your code, Copilot will do this for you.
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.8"
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pytest pytest-cov
|
||||||
|
|
||||||
|
- name: Validate Python syntax
|
||||||
|
run: |
|
||||||
|
python3 -c "import ast; ast.parse(open('styles_csv_loader.py').read()); print('styles_csv_loader.py syntax valid')"
|
||||||
|
python3 -c "import ast; ast.parse(open('__init__.py').read()); print('__init__.py syntax valid')"
|
||||||
@@ -3,7 +3,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- master
|
||||||
paths:
|
paths:
|
||||||
- "pyproject.toml"
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pytest pytest-cov
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest tests/ -v --cov=. --cov-report=xml --cov-report=term-missing
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
if: matrix.python-version == '3.12'
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
fail_ci_if_error: false
|
||||||
+15
-1
@@ -1 +1,15 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# Styles CSV Loader Extension for ComfyUI
|
# Styles CSV Loader Extension for ComfyUI
|
||||||
Extension for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that loads styles from a CSV file.
|
Extension for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that loads styles from a CSV file.
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
This extension allows users to load styles from a CSV file (styles.csv), primarily for migration purposes from the [automatic1111 Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui).
|
This extension allows users to load styles from a CSV file (styles.csv), primarily for migration purposes from the [automatic1111 Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui).
|
||||||
|
|
||||||
@@ -8,9 +9,54 @@ This extension allows users to load styles from a CSV file (styles.csv), primari
|
|||||||
- OR: Use the [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) to install this extension.
|
- OR: Use the [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) to install this extension.
|
||||||
|
|
||||||
**Important**: The `styles.csv` file must be located in the root of `ComfyUI` where `main.py` resides.
|
**Important**: The `styles.csv` file must be located in the root of `ComfyUI` where `main.py` resides.
|
||||||
|
|
||||||
## Nodes Description
|
## Nodes Description
|
||||||
Each style is represented as a dictionary with the keys being `style_name` and the values being a list containing `positive_prompt` and `negative_prompt`. The prompts are outputs of this Node.
|
Each style is represented as a dictionary with the keys being `style_name` and the values being a list containing `positive_prompt` and `negative_prompt`. The prompts are outputs of this Node.
|
||||||
|
|
||||||
|
## CSV Format
|
||||||
|
The CSV file should have the following format:
|
||||||
|
```csv
|
||||||
|
name,prompt,negative_prompt
|
||||||
|
"Style Name","positive prompt text","negative prompt text"
|
||||||
|
"Another Style","more positive text","more negative text"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Setting up the development environment
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install test dependencies:
|
||||||
|
```bash
|
||||||
|
pip install pytest pytest-cov
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
Run all tests:
|
||||||
|
```bash
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests with coverage:
|
||||||
|
```bash
|
||||||
|
pytest tests/ -v --cov=. --cov-report=term-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
- `tests/test_styles_csv_loader.py` - Main test file for the StylesCSVLoader class
|
||||||
|
- `tests/fixtures/` - Example CSV files for testing:
|
||||||
|
- `valid_styles.csv` - Properly formatted CSV with valid styles
|
||||||
|
- `invalid_styles.csv` - CSV with missing columns to test error handling
|
||||||
|
- `complex_styles.csv` - CSV with complex content (quotes, special characters)
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
The project uses GitHub Actions for CI/CD. Tests are automatically run on:
|
||||||
|
- Python 3.8, 3.9, 3.10, 3.11, and 3.12
|
||||||
|
- Every push to main branch
|
||||||
|
- Every pull request
|
||||||
|
|
||||||
|
### Code Coverage
|
||||||
|
The project maintains 100% test coverage. Coverage reports are generated during CI runs.
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
- David Fischer
|
- David Fischer
|
||||||
- GitHub: [theUpsider](https://github.com/theUpsider)
|
- GitHub: [theUpsider](https://github.com/theUpsider)
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
from .styles_csv_loader import *
|
from styles_csv_loader import StylesCSVLoader
|
||||||
+38
-1
@@ -1,14 +1,51 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-styles_csv_loader"
|
name = "comfyui-styles_csv_loader"
|
||||||
description = "This extension allows users to load styles from a CSV file, primarily for migration purposes from the automatic1111 Stable Diffusion web UI."
|
description = "This extension allows users to load styles from a CSV file, primarily for migration purposes from the automatic1111 Stable Diffusion web UI."
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
license = { text = "MIT License" }
|
license = { text = "MIT License" }
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"pytest>=7.0.0",
|
||||||
|
"pytest-cov>=4.0.0"
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Repository = "https://github.com/theUpsider/ComfyUI-Styles_CSV_Loader"
|
Repository = "https://github.com/theUpsider/ComfyUI-Styles_CSV_Loader"
|
||||||
# Used by Comfy Registry https://comfyregistry.org
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
py-modules = ["styles_csv_loader"]
|
||||||
|
|
||||||
[tool.comfy]
|
[tool.comfy]
|
||||||
PublisherId = "theupsider"
|
PublisherId = "theupsider"
|
||||||
DisplayName = "ComfyUI-Styles_CSV_Loader"
|
DisplayName = "ComfyUI-Styles_CSV_Loader"
|
||||||
Icon = ""
|
Icon = ""
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"-v",
|
||||||
|
"--tb=short",
|
||||||
|
"--strict-markers",
|
||||||
|
"--disable-warnings"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["."]
|
||||||
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
"*/test_*",
|
||||||
|
"*/__pycache__/*"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise AssertionError",
|
||||||
|
"raise NotImplementedError"
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test runner script for ComfyUI-Styles_CSV_Loader
|
||||||
|
Usage: python run_tests.py [--coverage]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run tests with optional coverage reporting."""
|
||||||
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
base_cmd = [sys.executable, "-m", "pytest", "tests/", "-v"]
|
||||||
|
|
||||||
|
if "--coverage" in sys.argv:
|
||||||
|
base_cmd.extend(["--cov=.", "--cov-report=term-missing", "--cov-report=html"])
|
||||||
|
print("Running tests with coverage...")
|
||||||
|
else:
|
||||||
|
print("Running tests...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(base_cmd, check=True)
|
||||||
|
print("\n✅ All tests passed!")
|
||||||
|
if "--coverage" in sys.argv:
|
||||||
|
print("📊 Coverage report generated in htmlcov/")
|
||||||
|
return 0
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"\n❌ Tests failed with exit code {e.returncode}")
|
||||||
|
return e.returncode
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+31
-62
@@ -2,83 +2,56 @@ import os
|
|||||||
import re
|
import re
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
|
|
||||||
class StylesCSVLoader:
|
class StylesCSVLoader:
|
||||||
"""
|
"""
|
||||||
Loads csv file with styles. For migration purposes from automatic1111 webui.
|
Loads csv file with styles. For migration purposes from automatic11111 webui.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_styles_csv(styles_path: str):
|
def load_styles_csv(styles_path: str):
|
||||||
"""Loads csv file with styles. Each row has three columns:
|
"""Loads csv file with styles. It has only one column.
|
||||||
- style_name: string (labeled as "name" in the CSV)
|
Ignore the first row (header).
|
||||||
- positive_prompt: string (labeled as "prompt" in the CSV)
|
positive_prompt are strings separated by comma. Each string is a prompt.
|
||||||
- negative_prompt: string (may be empty or missing)
|
negative_prompt are strings separated by comma. Each string is a prompt.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Dictionary of styles, where each key is style_name and value is [positive_prompt, negative_prompt]
|
list: List of styles. Each style is a dict with keys: style_name and value: [positive_prompt, negative_prompt]
|
||||||
"""
|
"""
|
||||||
error_styles = {
|
styles = {"Error loading styles.csv, check the console": ["", ""]}
|
||||||
"Error loading styles.csv, check the console": ["", ""]}
|
|
||||||
|
|
||||||
if not os.path.exists(styles_path):
|
if not os.path.exists(styles_path):
|
||||||
|
# Normalize path for cross-platform display
|
||||||
|
normalized_base_path = os.path.normpath(folder_paths.base_path)
|
||||||
print(f"""Error. No styles.csv found. Put your styles.csv in the root directory of ComfyUI. Then press "Refresh".
|
print(f"""Error. No styles.csv found. Put your styles.csv in the root directory of ComfyUI. Then press "Refresh".
|
||||||
Your current root directory is: {folder_paths.base_path}
|
Your current root directory is: {normalized_base_path}
|
||||||
""")
|
""")
|
||||||
return error_styles
|
|
||||||
|
|
||||||
try:
|
|
||||||
styles = {}
|
|
||||||
with open(styles_path, "r", encoding="utf-8") as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
next(reader) # Skip header row
|
|
||||||
|
|
||||||
# Start counting from 2 (after header)
|
|
||||||
for i, row in enumerate(reader, start=2):
|
|
||||||
try:
|
|
||||||
# Initialize variables to avoid "used before assigned" errors
|
|
||||||
style_name = ""
|
|
||||||
positive_prompt = ""
|
|
||||||
negative_prompt = ""
|
|
||||||
|
|
||||||
# Handle rows with fewer than 3 columns
|
|
||||||
if len(row) == 0:
|
|
||||||
# Skip empty rows
|
|
||||||
continue
|
|
||||||
elif len(row) == 1:
|
|
||||||
print(
|
|
||||||
f"Error parsing line {i}: {','.join(row)} -> Expected at least 2 columns, got {len(row)}")
|
|
||||||
continue
|
|
||||||
elif len(row) == 2:
|
|
||||||
# Missing negative prompt - treat as empty string
|
|
||||||
style_name, positive_prompt = row
|
|
||||||
# negative_prompt already initialized as empty string
|
|
||||||
elif len(row) >= 3:
|
|
||||||
# Normal case or extra columns (ignore extras)
|
|
||||||
style_name, positive_prompt, negative_prompt = row[0], row[1], row[2]
|
|
||||||
|
|
||||||
styles[style_name] = [positive_prompt, negative_prompt]
|
|
||||||
except Exception as e:
|
|
||||||
# Use a safe version of the row for error reporting
|
|
||||||
row_str = ','.join(row) if isinstance(
|
|
||||||
row, list) else str(row)
|
|
||||||
print(f"Error parsing line {i}: {row_str} -> {e}")
|
|
||||||
|
|
||||||
return styles
|
return styles
|
||||||
|
try:
|
||||||
|
with open(styles_path, "r", encoding="utf-8") as f:
|
||||||
|
styles = [[x.replace('"', '').replace('\n', '') for x in re.split(
|
||||||
|
',(?=(?:[^"]*"[^"]*")*[^"]*$)', line)] for line in f.readlines()[1:]]
|
||||||
|
styles = {x[0]: [x[1], x[2]] for x in styles}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Normalize path for cross-platform display
|
||||||
|
normalized_base_path = os.path.normpath(folder_paths.base_path)
|
||||||
print(f"""Error loading styles.csv. Make sure it is in the root directory of ComfyUI. Then press "Refresh".
|
print(f"""Error loading styles.csv. Make sure it is in the root directory of ComfyUI. Then press "Refresh".
|
||||||
Your current root directory is: {folder_paths.base_path}
|
Your current root directory is: {normalized_base_path}
|
||||||
Error: {e}
|
Error: {e}
|
||||||
""")
|
""")
|
||||||
return error_styles
|
# Return the error dictionary instead of the intermediate styles list
|
||||||
|
styles = {"Error loading styles.csv, check the console": ["", ""]}
|
||||||
|
return styles
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
cls.styles_csv = cls.load_styles_csv(
|
# Use os.path.normpath to ensure cross-platform compatibility
|
||||||
os.path.join(folder_paths.base_path, "styles.csv"))
|
styles_path = os.path.normpath(os.path.join(folder_paths.base_path, "styles.csv"))
|
||||||
|
cls.styles_csv = cls.load_styles_csv(styles_path)
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"styles": (list(cls.styles_csv.keys()),),
|
"styles": (list(cls.styles_csv.keys()),),
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("STRING", "STRING")
|
RETURN_TYPES = ("STRING", "STRING")
|
||||||
@@ -87,16 +60,12 @@ Error: {e}
|
|||||||
CATEGORY = "loaders"
|
CATEGORY = "loaders"
|
||||||
|
|
||||||
def execute(self, styles):
|
def execute(self, styles):
|
||||||
return (
|
return (self.styles_csv[styles][0], self.styles_csv[styles][1])
|
||||||
self.styles_csv[styles][0],
|
|
||||||
self.styles_csv[styles][1]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"Load Styles CSV": StylesCSVLoader
|
"Load Styles CSV": StylesCSVLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"StylesCSVLoader": "Load Styles CSV Node"
|
"StylesCSVLoader": "Load Styles CSV Node"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Test package for ComfyUI-Styles_CSV_Loader
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
name,prompt,negative_prompt
|
||||||
|
"Complex Quotes","painting with ""double quotes"" and, commas","bad quality, ""ugly"" result"
|
||||||
|
"Special Characters","ñáéíóú àèìòù çñ","low quality"
|
||||||
|
"Empty Negative","beautiful artwork, masterpiece",""
|
||||||
|
"Multi-line Style","very long prompt that contains many details, high quality, award winning, masterpiece, detailed textures","low quality, blurry, amateur, bad composition, poor lighting"
|
||||||
|
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
name,prompt
|
||||||
|
"Missing Column","this CSV only has 2 columns instead of 3"
|
||||||
|
"Also Missing","another incomplete row"
|
||||||
|
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
name,prompt,negative_prompt
|
||||||
|
"Cinematic","cinematic shot, dramatic lighting, film grain","low quality, blurry, amateur"
|
||||||
|
"Portrait","portrait photography, professional lighting, sharp focus","ugly, deformed, extra limbs"
|
||||||
|
"Landscape","landscape photography, golden hour, wide angle","people, buildings, urban"
|
||||||
|
"Anime Style","anime art style, vibrant colors, detailed","realistic, photographic, western art"
|
||||||
|
"Oil Painting","oil painting style, textured brushstrokes, classical","digital art, photograph, modern"
|
||||||
|
@@ -0,0 +1,158 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# Add the parent directory to the path so we can import the module
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# Mock the folder_paths module before importing styles_csv_loader
|
||||||
|
sys.modules['folder_paths'] = MagicMock()
|
||||||
|
|
||||||
|
from styles_csv_loader import StylesCSVLoader
|
||||||
|
|
||||||
|
|
||||||
|
class TestStylesCSVLoader(unittest.TestCase):
|
||||||
|
"""Test cases for StylesCSVLoader class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
self.fixtures_dir = os.path.join(self.test_dir, 'fixtures')
|
||||||
|
self.valid_csv_path = os.path.join(self.fixtures_dir, 'valid_styles.csv')
|
||||||
|
self.invalid_csv_path = os.path.join(self.fixtures_dir, 'invalid_styles.csv')
|
||||||
|
self.complex_csv_path = os.path.join(self.fixtures_dir, 'complex_styles.csv')
|
||||||
|
self.nonexistent_csv_path = os.path.join(self.fixtures_dir, 'nonexistent.csv')
|
||||||
|
|
||||||
|
def test_load_valid_csv(self):
|
||||||
|
"""Test loading a valid CSV file."""
|
||||||
|
styles = StylesCSVLoader.load_styles_csv(self.valid_csv_path)
|
||||||
|
|
||||||
|
# Check that styles were loaded
|
||||||
|
self.assertIsInstance(styles, dict)
|
||||||
|
self.assertGreater(len(styles), 0)
|
||||||
|
|
||||||
|
# Check specific style exists
|
||||||
|
self.assertIn('Cinematic', styles)
|
||||||
|
|
||||||
|
# Check structure of a style
|
||||||
|
cinematic_style = styles['Cinematic']
|
||||||
|
self.assertIsInstance(cinematic_style, list)
|
||||||
|
self.assertEqual(len(cinematic_style), 2) # positive and negative prompt
|
||||||
|
|
||||||
|
# Check content
|
||||||
|
self.assertEqual(cinematic_style[0], 'cinematic shot, dramatic lighting, film grain')
|
||||||
|
self.assertEqual(cinematic_style[1], 'low quality, blurry, amateur')
|
||||||
|
|
||||||
|
def test_load_invalid_csv(self):
|
||||||
|
"""Test loading an invalid CSV file (missing columns). Should return an error dictionary."""
|
||||||
|
styles = StylesCSVLoader.load_styles_csv(self.invalid_csv_path)
|
||||||
|
|
||||||
|
# The correct behavior: should return a dict with an error message
|
||||||
|
self.assertIsInstance(styles, dict)
|
||||||
|
error_key = list(styles.keys())[0]
|
||||||
|
self.assertIn('Error loading styles.csv', error_key)
|
||||||
|
# Style values should always be lists with positive and negative prompts
|
||||||
|
self.assertIsInstance(styles[error_key], list)
|
||||||
|
self.assertEqual(len(styles[error_key]), 2)
|
||||||
|
|
||||||
|
def test_load_invalid_csv_buggy_behavior(self):
|
||||||
|
"""[BUG DOCUMENTATION] Current buggy behavior: returns a list instead of error dict for invalid CSV.
|
||||||
|
Remove this test once the bug in styles_csv_loader.py is fixed."""
|
||||||
|
styles = StylesCSVLoader.load_styles_csv(self.invalid_csv_path)
|
||||||
|
# This documents the buggy behavior for regression tracking.
|
||||||
|
if isinstance(styles, list):
|
||||||
|
self.assertGreater(len(styles), 0)
|
||||||
|
self.assertIn('Missing Column', styles[0])
|
||||||
|
else:
|
||||||
|
# If bug is fixed, this test should be removed.
|
||||||
|
self.skipTest("Bug fixed: loader returns error dict instead of list.")
|
||||||
|
def test_load_nonexistent_csv(self):
|
||||||
|
"""Test loading a nonexistent CSV file."""
|
||||||
|
styles = StylesCSVLoader.load_styles_csv(self.nonexistent_csv_path)
|
||||||
|
|
||||||
|
# Should return error message in styles
|
||||||
|
self.assertIsInstance(styles, dict)
|
||||||
|
error_key = list(styles.keys())[0]
|
||||||
|
self.assertIn('Error loading styles.csv', error_key)
|
||||||
|
|
||||||
|
def test_load_complex_csv(self):
|
||||||
|
"""Test loading a CSV with complex content (quotes, special chars)."""
|
||||||
|
styles = StylesCSVLoader.load_styles_csv(self.complex_csv_path)
|
||||||
|
|
||||||
|
# Check that styles were loaded
|
||||||
|
self.assertIsInstance(styles, dict)
|
||||||
|
self.assertGreater(len(styles), 0)
|
||||||
|
|
||||||
|
# Check complex quotes handling
|
||||||
|
self.assertIn('Complex Quotes', styles)
|
||||||
|
complex_style = styles['Complex Quotes']
|
||||||
|
self.assertIn('double quotes', complex_style[0])
|
||||||
|
|
||||||
|
# Check special characters
|
||||||
|
self.assertIn('Special Characters', styles)
|
||||||
|
special_style = styles['Special Characters']
|
||||||
|
self.assertIn('ñáéíóú', special_style[0])
|
||||||
|
|
||||||
|
# Check empty negative prompt
|
||||||
|
self.assertIn('Empty Negative', styles)
|
||||||
|
empty_neg_style = styles['Empty Negative']
|
||||||
|
self.assertEqual(empty_neg_style[1], '')
|
||||||
|
|
||||||
|
def test_all_styles_have_correct_structure(self):
|
||||||
|
"""Test that all loaded styles have the correct structure."""
|
||||||
|
styles = StylesCSVLoader.load_styles_csv(self.valid_csv_path)
|
||||||
|
|
||||||
|
for style_name, style_data in styles.items():
|
||||||
|
# Each style should be a list with exactly 2 elements
|
||||||
|
self.assertIsInstance(style_data, list, f"Style '{style_name}' should be a list")
|
||||||
|
self.assertEqual(len(style_data), 2, f"Style '{style_name}' should have exactly 2 elements")
|
||||||
|
|
||||||
|
# Both elements should be strings
|
||||||
|
self.assertIsInstance(style_data[0], str, f"Positive prompt for '{style_name}' should be a string")
|
||||||
|
self.assertIsInstance(style_data[1], str, f"Negative prompt for '{style_name}' should be a string")
|
||||||
|
|
||||||
|
@patch('styles_csv_loader.folder_paths')
|
||||||
|
def test_input_types_with_valid_csv(self, mock_folder_paths):
|
||||||
|
"""Test INPUT_TYPES method with valid CSV."""
|
||||||
|
mock_folder_paths.base_path = self.fixtures_dir
|
||||||
|
|
||||||
|
# Create a temporary styles.csv in the fixtures directory
|
||||||
|
temp_csv_path = os.path.join(self.fixtures_dir, 'styles.csv')
|
||||||
|
with open(temp_csv_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('name,prompt,negative_prompt\n')
|
||||||
|
f.write('"Test Style","test prompt","test negative"\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
input_types = StylesCSVLoader.INPUT_TYPES()
|
||||||
|
|
||||||
|
# Check structure
|
||||||
|
self.assertIn('required', input_types)
|
||||||
|
self.assertIn('styles', input_types['required'])
|
||||||
|
|
||||||
|
# Check that styles list contains our test style
|
||||||
|
styles_list = input_types['required']['styles'][0]
|
||||||
|
self.assertIn('Test Style', styles_list)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up
|
||||||
|
if os.path.exists(temp_csv_path):
|
||||||
|
os.remove(temp_csv_path)
|
||||||
|
|
||||||
|
def test_execute_method(self):
|
||||||
|
"""Test the execute method returns correct prompts."""
|
||||||
|
loader = StylesCSVLoader()
|
||||||
|
|
||||||
|
# Mock the styles_csv attribute
|
||||||
|
loader.styles_csv = {
|
||||||
|
'Test Style': ['positive prompt here', 'negative prompt here']
|
||||||
|
}
|
||||||
|
|
||||||
|
positive, negative = loader.execute('Test Style')
|
||||||
|
|
||||||
|
self.assertEqual(positive, 'positive prompt here')
|
||||||
|
self.assertEqual(negative, 'negative prompt here')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user