Merge branch 'main' into pyproject
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'gmorks' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
+6
-1
@@ -1 +1,6 @@
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
To-Do.md
|
||||
logs/
|
||||
config/animagine_state.json
|
||||
config/csv_history.json
|
||||
logs/animagine_prompt_20250130.log
|
||||
|
||||
@@ -1,4 +1,104 @@
|
||||
## ComfyUI Animagine prompt
|
||||
- ComfyUI note to style your positive and negative prompts with the suggested guidelines on the page model for [Animagine-xl-4.0](https://huggingface.co/cagliostrolab/animagine-xl-4.0)
|
||||
# ComfyUI Animagine Prompt Node
|
||||
|
||||
A ComfyUI node designed to help you structure your prompts following the recommended guidelines for the [Animagine-XL-4.0](https://huggingface.co/cagliostrolab/animagine-xl-4.0) model. This node makes it easy to maintain consistency in your prompts and experiment with different combinations of quality and style tags.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Tag Management**: Easily add and combine quality tags, score tags, and other important modifiers
|
||||
- **CSV Character Integration**: Load and use character descriptions from CSV files for consistent character generation
|
||||
- **Flexible Controls**: Toggle different tag groups on/off as needed
|
||||
- **Built-in Best Practices**: Incorporates recommended negative prompts and tag structures
|
||||
|
||||
## Installation
|
||||
|
||||
1. Navigate to your ComfyUI custom nodes directory
|
||||
2. Clone this repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/ComfyUI-Animagine-Prompt.git
|
||||
```
|
||||
3. Restart ComfyUI
|
||||
|
||||
## Using the Node
|
||||
|
||||
### Basic Usage
|
||||
|
||||
1. Add the "Animagine Prompt" node to your workflow
|
||||
2. Enter your base prompt in the "positive_prompt" field
|
||||
3. Toggle the desired tag groups (Good Tags, Bad Tags, etc.)
|
||||
4. Connect the outputs to your text input nodes
|
||||
|
||||
### Tag Groups
|
||||
|
||||
The node provides several tag groups you can mix and match:
|
||||
|
||||
- **Good Tags**
|
||||
- Quality tags (masterpiece, best quality)
|
||||
- Score tags (high score, great score, good score)
|
||||
- **Bad Tags**
|
||||
- Quality tags (low quality, worst quality)
|
||||
- Score tags (average score, bad score, low score)
|
||||
- **Rating Tags**
|
||||
- Safe, Sensitive, NSFW, Explicit
|
||||
- **Year Tag**
|
||||
- Specify a year to influence the style
|
||||
|
||||
### Using CSV Character Data
|
||||
|
||||
You can load character descriptions from a CSV file to maintain consistency in character generation:
|
||||
|
||||
1. Enable the CSV feature using the "use_csv" toggle
|
||||
2. Provide the path to your CSV file
|
||||
3. Select the character entry using the index selector
|
||||
|
||||
#### CSV File Structure
|
||||
|
||||
Your CSV file should have the following columns:
|
||||
```
|
||||
GENDER,CHARACTER,COPYRIGHT
|
||||
1girl,character name,series name
|
||||
1boy,another character,another series
|
||||
```
|
||||
|
||||
See `create_your_own.txt` for detailed instructions on creating and editing CSV files.
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
- Start with the basic tags enabled and gradually experiment with different combinations
|
||||
- Keep your base prompts clean and focused - the node will handle the structural elements
|
||||
- Use the CSV feature for characters you generate frequently to maintain consistency
|
||||
- Monitor the logs for any issues with CSV loading or prompt generation
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**CSV file not loading:**
|
||||
- Check that the file path is correct
|
||||
- Ensure the CSV has the required columns: GENDER, CHARACTER, COPYRIGHT
|
||||
- Verify the file is saved in UTF-8 format
|
||||
|
||||
**Prompts not generating as expected:**
|
||||
- Check that the appropriate tag groups are enabled
|
||||
- Verify your base prompt doesn't contain conflicting tags
|
||||
- Look for any trailing commas or spaces in your prompts
|
||||
|
||||
### Performance Notes
|
||||
|
||||
- Large CSV files may impact loading times
|
||||
- The node includes a simple caching system for CSV data
|
||||
- Consider splitting very large CSV files into smaller, focused collections
|
||||
|
||||
## Support
|
||||
|
||||
Found a bug or have a suggestion? Please open an issue on the GitHub repository.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Thanks to the Animagine-XL model creators for their guidelines
|
||||
- Thanks to the ComfyUI community for their support and feedback
|
||||
+6
-6
@@ -7,29 +7,29 @@ import os
|
||||
import logging
|
||||
from .animagine_node import AnimaginePromptNode
|
||||
|
||||
# Configuración básica de logging
|
||||
# Basic logging configuration
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('AnimaginePrompt')
|
||||
|
||||
# Directorio base del nodo
|
||||
# Base directory of the node
|
||||
NODE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
SAMPLE_DATA_DIR = os.path.join(NODE_DIR, "sample_data")
|
||||
|
||||
# Asegurarse de que existe el directorio de sample_data
|
||||
# Ensure the sample_data directory exists
|
||||
if not os.path.exists(SAMPLE_DATA_DIR):
|
||||
os.makedirs(SAMPLE_DATA_DIR)
|
||||
|
||||
# Mapping para el registro del nodo
|
||||
# Mapping for node registration
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AnimaginePrompt": AnimaginePromptNode
|
||||
}
|
||||
|
||||
# Mapping para el nombre de visualización
|
||||
# Mapping for display name
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AnimaginePrompt": "Animagine Prompt"
|
||||
}
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.0.1"
|
||||
+145
-35
@@ -1,27 +1,28 @@
|
||||
"""
|
||||
Main node implementation for Animagine Prompt
|
||||
Provides prompt structuring and enhancement functionality following Animagine model guidelines
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Tuple, Dict, Any, List, Optional
|
||||
import logging
|
||||
from .csv_handler import CSVHandler
|
||||
|
||||
logger = logging.getLogger('AnimaginePrompt')
|
||||
from .logger import logger
|
||||
from .state_manager import state_manager
|
||||
|
||||
class AnimaginePromptNode:
|
||||
"""
|
||||
A ComfyUI node that helps structure prompts according to specific guidelines
|
||||
"""
|
||||
|
||||
# Definición de tags predefinidas
|
||||
# Predefined tag sets
|
||||
QUALITY_TAGS_GOOD = ["masterpiece", "best quality"]
|
||||
QUALITY_TAGS_BAD = ["low quality", "worst quality"]
|
||||
SCORE_TAGS_GOOD = ["high score", "great score", "good score"]
|
||||
SCORE_TAGS_BAD = ["average score", "bad score", "low score"]
|
||||
RATING_TAGS = ["safe", "sensitive", "nsfw", "explicit"]
|
||||
|
||||
# Tags negativas por defecto según las guidelines
|
||||
# Default negative tags according to guidelines
|
||||
DEFAULT_NEGATIVE_TAGS = [
|
||||
"low quality", "worst quality", "blurry", "bad anatomy",
|
||||
"bad hands", "text", "error", "missing fingers",
|
||||
@@ -32,31 +33,39 @@ class AnimaginePromptNode:
|
||||
def __init__(self):
|
||||
self.csv_handler = CSVHandler()
|
||||
self.last_csv_path = None
|
||||
|
||||
# Load last state or use defaults
|
||||
self.current_state = state_manager.get_state()
|
||||
if not self.current_state:
|
||||
state_manager.reset_state()
|
||||
self.current_state = state_manager.get_state()
|
||||
logger.logger.info("AnimaginePromptNode initialized with saved state")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Dict[str, Any]]:
|
||||
"""Define input parameters and their types"""
|
||||
state = state_manager.get_state()
|
||||
return {
|
||||
"required": {
|
||||
"positive_prompt": ("STRING", {"multiline": True}),
|
||||
"negative_prompt": ("STRING", {"multiline": True}),
|
||||
# Good tags controls
|
||||
"use_good_tags": ("BOOLEAN", {"default": True}),
|
||||
"use_good_tags": ("BOOLEAN", {"default": state.get("use_good_tags", True)}),
|
||||
"quality_good": (cls.QUALITY_TAGS_GOOD,),
|
||||
"score_good": (cls.SCORE_TAGS_GOOD,),
|
||||
# Bad tags controls
|
||||
"use_bad_tags": ("BOOLEAN", {"default": False}),
|
||||
"use_bad_tags": ("BOOLEAN", {"default": state.get("use_bad_tags", False)}),
|
||||
"quality_bad": (cls.QUALITY_TAGS_BAD,),
|
||||
"score_bad": (cls.SCORE_TAGS_BAD,),
|
||||
# Year control
|
||||
"use_year": ("BOOLEAN", {"default": False}),
|
||||
"year": ("INT", {"default": 2024}),
|
||||
"use_year": ("BOOLEAN", {"default": state.get("use_year", False)}),
|
||||
"year": ("INT", {"default": state.get("year", 2024)}),
|
||||
# Rating control
|
||||
"use_rating": ("BOOLEAN", {"default": False}),
|
||||
"use_rating": ("BOOLEAN", {"default": state.get("use_rating", False)}),
|
||||
"rating": (cls.RATING_TAGS,),
|
||||
# CSV control
|
||||
"use_csv": ("BOOLEAN", {"default": False}),
|
||||
"csv_path": ("STRING", {"default": ""}),
|
||||
"csv_index": ("INT", {"default": 0, "min": 0, "max": 9999})
|
||||
"use_csv": ("BOOLEAN", {"default": state.get("use_csv", False)}),
|
||||
"csv_path": ("STRING", {"default": state.get("csv_path", "")}),
|
||||
"csv_index": ("INT", {"default": state.get("csv_index", 0), "min": 0, "max": 9999})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,32 +75,55 @@ class AnimaginePromptNode:
|
||||
CATEGORY = "prompt"
|
||||
|
||||
def _build_good_tags(self, quality_good: str, score_good: str) -> str:
|
||||
"""Construye la sección de tags positivas"""
|
||||
"""Build the positive tags section"""
|
||||
tags = []
|
||||
if quality_good:
|
||||
tags.append(quality_good)
|
||||
if score_good:
|
||||
tags.append(score_good)
|
||||
return ", ".join(tags)
|
||||
|
||||
result = ", ".join(tags)
|
||||
logger.logger.debug(f"Built good tags: {result}")
|
||||
return result
|
||||
|
||||
def _build_bad_tags(self, quality_bad: str, score_bad: str) -> str:
|
||||
"""Construye la sección de tags negativas"""
|
||||
"""Build the negative tags section"""
|
||||
tags = []
|
||||
if quality_bad:
|
||||
tags.append(quality_bad)
|
||||
if score_bad:
|
||||
tags.append(score_bad)
|
||||
return ", ".join(tags)
|
||||
|
||||
result = ", ".join(tags)
|
||||
logger.logger.debug(f"Built bad tags: {result}")
|
||||
return result
|
||||
|
||||
def _get_csv_entry(self, csv_path: str, index: int) -> Optional[str]:
|
||||
"""Obtiene y formatea una entrada del CSV"""
|
||||
"""Get and format an entry from the CSV file"""
|
||||
start_time = time.time()
|
||||
|
||||
if self.last_csv_path != csv_path:
|
||||
self.csv_handler.load_csv(csv_path)
|
||||
logger.logger.info(f"Loading new CSV file: {csv_path}")
|
||||
df = self.csv_handler.load_csv(csv_path)
|
||||
if df is not None:
|
||||
state_manager.add_csv_to_history(csv_path, len(df))
|
||||
self.last_csv_path = csv_path
|
||||
|
||||
entry = self.csv_handler.get_entry(index)
|
||||
|
||||
end_time = time.time()
|
||||
logger.log_performance(
|
||||
"CSV Entry Retrieval",
|
||||
start_time,
|
||||
end_time,
|
||||
{"path": csv_path, "index": index}
|
||||
)
|
||||
|
||||
if entry:
|
||||
return f"{entry['gender']}, {entry['character']}, {entry['copyright']}"
|
||||
formatted_entry = f"{entry['gender']}, {entry['character']}, {entry['copyright']}"
|
||||
logger.logger.debug(f"Formatted CSV entry: {formatted_entry}")
|
||||
return formatted_entry
|
||||
|
||||
return None
|
||||
|
||||
def generate_prompt(
|
||||
@@ -113,58 +145,136 @@ class AnimaginePromptNode:
|
||||
csv_index: int
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Genera los prompts finales basados en los parámetros proporcionados
|
||||
Generate final prompts based on provided parameters
|
||||
|
||||
Args:
|
||||
positive_prompt (str): Base positive prompt
|
||||
negative_prompt (str): Base negative prompt
|
||||
use_good_tags (bool): Whether to include good tags
|
||||
quality_good (str): Selected quality tag from good tags
|
||||
score_good (str): Selected score tag from good tags
|
||||
use_bad_tags (bool): Whether to include bad tags
|
||||
quality_bad (str): Selected quality tag from bad tags
|
||||
score_bad (str): Selected score tag from bad tags
|
||||
use_year (bool): Whether to include year tag
|
||||
year (int): Year to include in prompt
|
||||
use_rating (bool): Whether to include rating tag
|
||||
rating (str): Selected rating tag
|
||||
use_csv (bool): Whether to include CSV entry
|
||||
csv_path (str): Path to CSV file
|
||||
csv_index (int): Index of CSV entry to use
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Final positive and negative prompts
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Lista para construir el prompt positivo
|
||||
# Update state with current values
|
||||
state_manager.update_state({
|
||||
"use_good_tags": use_good_tags,
|
||||
"use_bad_tags": use_bad_tags,
|
||||
"use_year": use_year,
|
||||
"year": year,
|
||||
"use_rating": use_rating,
|
||||
"rating": rating,
|
||||
"use_csv": use_csv,
|
||||
"csv_path": csv_path,
|
||||
"csv_index": csv_index
|
||||
})
|
||||
|
||||
# Log initial state
|
||||
logger.log_prompt_generation(
|
||||
positive_prompt,
|
||||
negative_prompt,
|
||||
{
|
||||
"use_good_tags": use_good_tags,
|
||||
"use_bad_tags": use_bad_tags,
|
||||
"use_year": use_year,
|
||||
"use_rating": use_rating,
|
||||
"use_csv": use_csv
|
||||
}
|
||||
)
|
||||
|
||||
# Build positive prompt parts
|
||||
positive_parts = []
|
||||
|
||||
# Agregar tags positivas si están activadas
|
||||
# Add good tags if enabled
|
||||
if use_good_tags:
|
||||
good_tags = self._build_good_tags(quality_good, score_good)
|
||||
if good_tags:
|
||||
positive_parts.append(good_tags)
|
||||
logger.logger.debug(f"Added good tags: {good_tags}")
|
||||
|
||||
# Agregar bad tags al prompt positivo si están activadas
|
||||
# Add bad tags to positive prompt if enabled
|
||||
if use_bad_tags:
|
||||
bad_tags = self._build_bad_tags(quality_bad, score_bad)
|
||||
if bad_tags:
|
||||
positive_parts.append(bad_tags)
|
||||
logger.logger.debug(f"Added bad tags: {bad_tags}")
|
||||
|
||||
# Agregar entrada del CSV si está activado
|
||||
# Add CSV entry if enabled
|
||||
if use_csv and csv_path:
|
||||
csv_entry = self._get_csv_entry(csv_path, csv_index)
|
||||
if csv_entry:
|
||||
positive_parts.append(csv_entry)
|
||||
logger.logger.debug(f"Added CSV entry: {csv_entry}")
|
||||
|
||||
# Agregar el prompt base del usuario
|
||||
# Add base positive prompt
|
||||
if positive_prompt:
|
||||
positive_parts.append(positive_prompt)
|
||||
logger.logger.debug(f"Added base positive prompt: {positive_prompt}")
|
||||
|
||||
# Agregar year tag si está activado
|
||||
# Add year tag if enabled
|
||||
if use_year:
|
||||
positive_parts.append(f"year {year}")
|
||||
year_tag = f"year {year}"
|
||||
positive_parts.append(year_tag)
|
||||
logger.logger.debug(f"Added year tag: {year_tag}")
|
||||
|
||||
# Agregar rating si está activado
|
||||
# Add rating if enabled
|
||||
if use_rating:
|
||||
positive_parts.append(rating)
|
||||
logger.logger.debug(f"Added rating: {rating}")
|
||||
|
||||
# Construir el prompt negativo
|
||||
# Build negative prompt parts
|
||||
negative_parts = []
|
||||
|
||||
# Agregar tags negativas por defecto
|
||||
# Add default negative tags
|
||||
negative_parts.extend(self.DEFAULT_NEGATIVE_TAGS)
|
||||
logger.logger.debug("Added default negative tags")
|
||||
|
||||
# Agregar el prompt negativo del usuario
|
||||
# Add user negative prompt
|
||||
if negative_prompt:
|
||||
negative_parts.append(negative_prompt)
|
||||
logger.logger.debug(f"Added user negative prompt: {negative_prompt}")
|
||||
|
||||
# Unir todas las partes
|
||||
# Join all parts
|
||||
final_positive = ", ".join(part for part in positive_parts if part)
|
||||
final_negative = ", ".join(part for part in negative_parts if part)
|
||||
|
||||
end_time = time.time()
|
||||
logger.log_performance(
|
||||
"Prompt Generation",
|
||||
start_time,
|
||||
end_time,
|
||||
{
|
||||
"positive_parts": len(positive_parts),
|
||||
"negative_parts": len(negative_parts)
|
||||
}
|
||||
)
|
||||
|
||||
logger.logger.info("Prompt generation completed successfully")
|
||||
return final_positive, final_negative
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating prompt: {str(e)}")
|
||||
logger.log_error(
|
||||
e,
|
||||
"Prompt Generation",
|
||||
{
|
||||
"positive_prompt": positive_prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"use_csv": use_csv,
|
||||
"csv_path": csv_path
|
||||
}
|
||||
)
|
||||
raise e
|
||||
+65
-14
@@ -3,18 +3,17 @@ CSV handling functionality for Animagine Prompt Node
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import logging
|
||||
from typing import Optional, List, Dict
|
||||
from typing import Optional, Dict
|
||||
import os
|
||||
|
||||
logger = logging.getLogger('AnimaginePrompt')
|
||||
import time
|
||||
from .logger import logger
|
||||
|
||||
class CSVHandler:
|
||||
"""
|
||||
Handles loading and processing of CSV files containing character data
|
||||
"""
|
||||
|
||||
REQUIRED_COLUMNS = ['GENDER', 'CHARACTER', 'COPYRIGHT'] # Notar las mayúsculas
|
||||
REQUIRED_COLUMNS = ['GENDER', 'CHARACTER', 'COPYRIGHT']
|
||||
|
||||
def __init__(self):
|
||||
self.current_data = None
|
||||
@@ -26,21 +25,33 @@ class CSVHandler:
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(filepath):
|
||||
logger.error(f"CSV file not found: {filepath}")
|
||||
logger.log_error(
|
||||
FileNotFoundError(f"CSV file not found: {filepath}"),
|
||||
"CSV Validation",
|
||||
{"filepath": filepath}
|
||||
)
|
||||
return False
|
||||
|
||||
df = pd.read_csv(filepath)
|
||||
logger.info(f"CSV columns found: {df.columns.tolist()}")
|
||||
logger.log_csv_operation(
|
||||
"Validation",
|
||||
filepath,
|
||||
{"columns_found": df.columns.tolist()}
|
||||
)
|
||||
|
||||
missing_columns = [col for col in self.REQUIRED_COLUMNS if col not in df.columns]
|
||||
if missing_columns:
|
||||
logger.error(f"Missing required columns: {missing_columns}")
|
||||
logger.log_error(
|
||||
ValueError("Missing required columns"),
|
||||
"CSV Validation",
|
||||
{"missing_columns": missing_columns}
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating CSV structure: {str(e)}")
|
||||
logger.log_error(e, "CSV Validation", {"filepath": filepath})
|
||||
return False
|
||||
|
||||
def load_csv(self, filepath: str) -> Optional[pd.DataFrame]:
|
||||
@@ -48,20 +59,39 @@ class CSVHandler:
|
||||
Loads CSV file with basic caching
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
if filepath in self.cache:
|
||||
logger.log_csv_operation(
|
||||
"Cache Hit",
|
||||
filepath,
|
||||
{"cache_size": len(self.cache)}
|
||||
)
|
||||
return self.cache[filepath]
|
||||
|
||||
if not self.validate_csv_structure(filepath):
|
||||
logger.error(f"Invalid CSV structure in file: {filepath}")
|
||||
return None
|
||||
|
||||
df = pd.read_csv(filepath)
|
||||
self.cache[filepath] = df
|
||||
self.current_data = df
|
||||
|
||||
end_time = time.time()
|
||||
logger.log_performance(
|
||||
"CSV Load",
|
||||
start_time,
|
||||
end_time,
|
||||
{
|
||||
"filepath": filepath,
|
||||
"rows": len(df),
|
||||
"cached": True
|
||||
}
|
||||
)
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading CSV file: {str(e)}")
|
||||
logger.log_error(e, "CSV Load", {"filepath": filepath})
|
||||
return None
|
||||
|
||||
def get_entry(self, index: int) -> Optional[Dict[str, str]]:
|
||||
@@ -70,18 +100,39 @@ class CSVHandler:
|
||||
"""
|
||||
try:
|
||||
if self.current_data is None:
|
||||
logger.log_error(
|
||||
ValueError("No CSV data loaded"),
|
||||
"Get Entry",
|
||||
{"index": index}
|
||||
)
|
||||
return None
|
||||
|
||||
if index >= len(self.current_data):
|
||||
logger.error(f"Index {index} out of bounds for CSV with {len(self.current_data)} entries")
|
||||
logger.log_error(
|
||||
IndexError(f"Index {index} out of bounds"),
|
||||
"Get Entry",
|
||||
{
|
||||
"index": index,
|
||||
"max_index": len(self.current_data) - 1
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
row = self.current_data.iloc[index]
|
||||
return {
|
||||
entry = {
|
||||
'gender': row['GENDER'],
|
||||
'character': row['CHARACTER'],
|
||||
'copyright': row['COPYRIGHT']
|
||||
}
|
||||
|
||||
logger.log_csv_operation(
|
||||
"Get Entry",
|
||||
None,
|
||||
{"index": index, "entry": entry}
|
||||
)
|
||||
|
||||
return entry
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting CSV entry: {str(e)}")
|
||||
logger.log_error(e, "Get Entry", {"index": index})
|
||||
return None
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
logger.py - Logging system for ComfyUI Animagine Prompt Node
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
class AnimagineLogger:
|
||||
"""
|
||||
Custom logger for the Animagine Prompt Node with enhanced debugging capabilities
|
||||
"""
|
||||
|
||||
def __init__(self, log_dir: str = "logs"):
|
||||
self.log_dir = log_dir
|
||||
self.logger = None
|
||||
self.setup_logger()
|
||||
|
||||
def setup_logger(self) -> None:
|
||||
"""
|
||||
Initialize the logger with both file and console handlers
|
||||
"""
|
||||
# Create logs directory if it doesn't exist
|
||||
if not os.path.exists(self.log_dir):
|
||||
os.makedirs(self.log_dir)
|
||||
|
||||
# Create logger
|
||||
self.logger = logging.getLogger('AnimaginePrompt')
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Remove existing handlers if any
|
||||
if self.logger.handlers:
|
||||
self.logger.handlers.clear()
|
||||
|
||||
# File handler - detailed logging
|
||||
log_file = os.path.join(
|
||||
self.log_dir,
|
||||
f"animagine_prompt_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
)
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
# Console handler - important messages only
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_formatter = logging.Formatter(
|
||||
'%(levelname)s - %(message)s'
|
||||
)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
def log_prompt_generation(
|
||||
self,
|
||||
positive_prompt: str,
|
||||
negative_prompt: str,
|
||||
params: Dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Log prompt generation details
|
||||
"""
|
||||
self.logger.debug("=== Prompt Generation Started ===")
|
||||
self.logger.debug(f"Parameters used: {params}")
|
||||
self.logger.debug(f"Base positive prompt: {positive_prompt}")
|
||||
self.logger.debug(f"Base negative prompt: {negative_prompt}")
|
||||
|
||||
def log_csv_operation(
|
||||
self,
|
||||
operation: str,
|
||||
filepath: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Log CSV-related operations
|
||||
"""
|
||||
self.logger.debug(f"=== CSV Operation: {operation} ===")
|
||||
if filepath:
|
||||
self.logger.debug(f"File: {filepath}")
|
||||
if details:
|
||||
self.logger.debug(f"Details: {details}")
|
||||
|
||||
def log_error(
|
||||
self,
|
||||
error: Exception,
|
||||
context: str,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Log errors with context and details
|
||||
"""
|
||||
self.logger.error(f"=== Error in {context} ===")
|
||||
self.logger.error(f"Error type: {type(error).__name__}")
|
||||
self.logger.error(f"Error message: {str(error)}")
|
||||
if details:
|
||||
self.logger.error(f"Additional details: {details}")
|
||||
|
||||
def log_performance(
|
||||
self,
|
||||
operation: str,
|
||||
start_time: float,
|
||||
end_time: float,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Log performance metrics
|
||||
"""
|
||||
duration = end_time - start_time
|
||||
self.logger.debug(f"=== Performance: {operation} ===")
|
||||
self.logger.debug(f"Duration: {duration:.4f} seconds")
|
||||
if details:
|
||||
self.logger.debug(f"Details: {details}")
|
||||
|
||||
def log_state_change(
|
||||
self,
|
||||
component: str,
|
||||
old_state: Any,
|
||||
new_state: Any
|
||||
) -> None:
|
||||
"""
|
||||
Log state changes in the node
|
||||
"""
|
||||
self.logger.debug(f"=== State Change: {component} ===")
|
||||
self.logger.debug(f"Old state: {old_state}")
|
||||
self.logger.debug(f"New state: {new_state}")
|
||||
|
||||
# Singleton instance
|
||||
logger = AnimagineLogger()
|
||||
@@ -0,0 +1,40 @@
|
||||
2025-01-30 08:55:54,686 - AnimaginePrompt - DEBUG - === CSV Operation: Validation ===
|
||||
2025-01-30 08:55:54,687 - AnimaginePrompt - DEBUG - File: C:\Users\Alex\Desktop\anime_prompt\ComfyUI\custom_nodes\ComfyUI-Animagine-prompt\example_csv\example.csv
|
||||
2025-01-30 08:55:54,687 - AnimaginePrompt - DEBUG - Details: {'columns_found': ['GENDER', 'CHARACTER', 'COPYRIGHT']}
|
||||
2025-01-30 08:55:54,688 - AnimaginePrompt - DEBUG - === Performance: CSV Load ===
|
||||
2025-01-30 08:55:54,689 - AnimaginePrompt - DEBUG - Duration: 0.0150 seconds
|
||||
2025-01-30 08:55:54,689 - AnimaginePrompt - DEBUG - Details: {'filepath': 'C:\\Users\\Alex\\Desktop\\anime_prompt\\ComfyUI\\custom_nodes\\ComfyUI-Animagine-prompt\\example_csv\\example.csv', 'rows': 23, 'cached': True}
|
||||
2025-01-30 08:55:54,689 - AnimaginePrompt - DEBUG - === CSV Operation: Get Entry ===
|
||||
2025-01-30 08:55:54,690 - AnimaginePrompt - DEBUG - Details: {'index': 4, 'entry': {'gender': '1girl', 'character': 'mokota mememe', 'copyright': '.live'}}
|
||||
2025-01-30 08:59:14,187 - AnimaginePrompt - INFO - AnimaginePromptNode initialized
|
||||
2025-01-30 08:59:14,187 - AnimaginePrompt - DEBUG - === Prompt Generation Started ===
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Parameters used: {'use_good_tags': True, 'use_bad_tags': True, 'use_year': True, 'use_rating': True, 'use_csv': True}
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Base positive prompt: Positive Prompt
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Base negative prompt: Negative Prompt
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Built good tags: masterpiece, high score
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Added good tags: masterpiece, high score
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Built bad tags: low quality, average score
|
||||
2025-01-30 08:59:14,188 - AnimaginePrompt - DEBUG - Added bad tags: low quality, average score
|
||||
2025-01-30 08:59:14,189 - AnimaginePrompt - INFO - Loading new CSV file: C:\Users\Alex\Desktop\anime_prompt\ComfyUI\custom_nodes\ComfyUI-Animagine-prompt\example_csv\example.csv
|
||||
2025-01-30 08:59:14,192 - AnimaginePrompt - DEBUG - === CSV Operation: Validation ===
|
||||
2025-01-30 08:59:14,192 - AnimaginePrompt - DEBUG - File: C:\Users\Alex\Desktop\anime_prompt\ComfyUI\custom_nodes\ComfyUI-Animagine-prompt\example_csv\example.csv
|
||||
2025-01-30 08:59:14,193 - AnimaginePrompt - DEBUG - Details: {'columns_found': ['GENDER', 'CHARACTER', 'COPYRIGHT']}
|
||||
2025-01-30 08:59:14,194 - AnimaginePrompt - DEBUG - === Performance: CSV Load ===
|
||||
2025-01-30 08:59:14,194 - AnimaginePrompt - DEBUG - Duration: 0.0056 seconds
|
||||
2025-01-30 08:59:14,195 - AnimaginePrompt - DEBUG - Details: {'filepath': 'C:\\Users\\Alex\\Desktop\\anime_prompt\\ComfyUI\\custom_nodes\\ComfyUI-Animagine-prompt\\example_csv\\example.csv', 'rows': 23, 'cached': True}
|
||||
2025-01-30 08:59:14,195 - AnimaginePrompt - DEBUG - === CSV Operation: Get Entry ===
|
||||
2025-01-30 08:59:14,195 - AnimaginePrompt - DEBUG - Details: {'index': 4, 'entry': {'gender': '1girl', 'character': 'mokota mememe', 'copyright': '.live'}}
|
||||
2025-01-30 08:59:14,195 - AnimaginePrompt - DEBUG - === Performance: CSV Entry Retrieval ===
|
||||
2025-01-30 08:59:14,196 - AnimaginePrompt - DEBUG - Duration: 0.0066 seconds
|
||||
2025-01-30 08:59:14,196 - AnimaginePrompt - DEBUG - Details: {'path': 'C:\\Users\\Alex\\Desktop\\anime_prompt\\ComfyUI\\custom_nodes\\ComfyUI-Animagine-prompt\\example_csv\\example.csv', 'index': 4}
|
||||
2025-01-30 08:59:14,196 - AnimaginePrompt - DEBUG - Formatted CSV entry: 1girl, mokota mememe, .live
|
||||
2025-01-30 08:59:14,196 - AnimaginePrompt - DEBUG - Added CSV entry: 1girl, mokota mememe, .live
|
||||
2025-01-30 08:59:14,196 - AnimaginePrompt - DEBUG - Added base positive prompt: Positive Prompt
|
||||
2025-01-30 08:59:14,197 - AnimaginePrompt - DEBUG - Added year tag: year 2024
|
||||
2025-01-30 08:59:14,197 - AnimaginePrompt - DEBUG - Added rating: safe
|
||||
2025-01-30 08:59:14,197 - AnimaginePrompt - DEBUG - Added default negative tags
|
||||
2025-01-30 08:59:14,197 - AnimaginePrompt - DEBUG - Added user negative prompt: Negative Prompt
|
||||
2025-01-30 08:59:14,197 - AnimaginePrompt - DEBUG - === Performance: Prompt Generation ===
|
||||
2025-01-30 08:59:14,198 - AnimaginePrompt - DEBUG - Duration: 0.0101 seconds
|
||||
2025-01-30 08:59:14,198 - AnimaginePrompt - DEBUG - Details: {'positive_parts': 6, 'negative_parts': 17}
|
||||
2025-01-30 08:59:14,198 - AnimaginePrompt - INFO - Prompt generation completed successfully
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "animagine-prompt"
|
||||
description = "Comfy UI node to prompt build for [a/https://huggingface.co/cagliostrolab/animagine-xl-4.0](https://huggingface.co/cagliostrolab/animagine-xl-4.0) model"
|
||||
version = "1.0.0"
|
||||
description = "Comfy UI node to prompt build for https://huggingface.co/cagliostrolab/animagine-xl-4.0 model "
|
||||
version = "1.0.1"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
@@ -9,6 +9,6 @@ Repository = "https://github.com/gmorks/ComfyUI-Animagine-Prompt"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = ""
|
||||
PublisherId = "gmorks"
|
||||
DisplayName = "ComfyUI-Animagine-Prompt"
|
||||
Icon = ""
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
state_manager.py - State management for ComfyUI Animagine Prompt Node
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from .logger import logger
|
||||
|
||||
class StateManager:
|
||||
"""
|
||||
Manages the persistence and restoration of node states
|
||||
"""
|
||||
|
||||
def __init__(self, config_dir: str = "config"):
|
||||
self.config_dir = config_dir
|
||||
self.state_file = os.path.join(config_dir, "animagine_state.json")
|
||||
self.history_file = os.path.join(config_dir, "csv_history.json")
|
||||
self.current_state: Dict[str, Any] = {}
|
||||
self.csv_history: Dict[str, Dict[str, Any]] = {}
|
||||
self._initialize()
|
||||
|
||||
def _initialize(self) -> None:
|
||||
"""Initialize the state system"""
|
||||
# Create config directory if it doesn't exist
|
||||
if not os.path.exists(self.config_dir):
|
||||
os.makedirs(self.config_dir)
|
||||
|
||||
# Load existing state if available
|
||||
self._load_state()
|
||||
self._load_csv_history()
|
||||
|
||||
def _load_state(self) -> None:
|
||||
"""Load the last saved state"""
|
||||
try:
|
||||
if os.path.exists(self.state_file):
|
||||
with open(self.state_file, 'r', encoding='utf-8') as f:
|
||||
self.current_state = json.load(f)
|
||||
logger.logger.debug("State loaded successfully")
|
||||
except Exception as e:
|
||||
logger.log_error(e, "State Loading")
|
||||
self.current_state = {}
|
||||
|
||||
def _load_csv_history(self) -> None:
|
||||
"""Load CSV usage history"""
|
||||
try:
|
||||
if os.path.exists(self.history_file):
|
||||
with open(self.history_file, 'r', encoding='utf-8') as f:
|
||||
self.csv_history = json.load(f)
|
||||
logger.logger.debug("CSV history loaded successfully")
|
||||
except Exception as e:
|
||||
logger.log_error(e, "CSV History Loading")
|
||||
self.csv_history = {}
|
||||
|
||||
def _save_state(self) -> None:
|
||||
"""Save current state to file"""
|
||||
try:
|
||||
with open(self.state_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.current_state, f, indent=2)
|
||||
logger.logger.debug("State saved successfully")
|
||||
except Exception as e:
|
||||
logger.log_error(e, "State Saving")
|
||||
|
||||
def _save_csv_history(self) -> None:
|
||||
"""Save CSV history to file"""
|
||||
try:
|
||||
with open(self.history_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.csv_history, f, indent=2)
|
||||
logger.logger.debug("CSV history saved successfully")
|
||||
except Exception as e:
|
||||
logger.log_error(e, "CSV History Saving")
|
||||
|
||||
def update_state(self, new_state: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Update the current state with new values
|
||||
"""
|
||||
old_state = self.current_state.copy()
|
||||
self.current_state.update(new_state)
|
||||
self._save_state()
|
||||
|
||||
logger.log_state_change(
|
||||
"Node State",
|
||||
old_state,
|
||||
self.current_state
|
||||
)
|
||||
|
||||
def get_state(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the current state
|
||||
"""
|
||||
return self.current_state
|
||||
|
||||
def reset_state(self) -> None:
|
||||
"""
|
||||
Reset state to default values
|
||||
"""
|
||||
default_state = {
|
||||
"use_good_tags": True,
|
||||
"use_bad_tags": False,
|
||||
"use_year": False,
|
||||
"year": 2024,
|
||||
"use_rating": False,
|
||||
"rating": "safe",
|
||||
"use_csv": False,
|
||||
"csv_path": "",
|
||||
"csv_index": 0
|
||||
}
|
||||
|
||||
old_state = self.current_state.copy()
|
||||
self.current_state = default_state
|
||||
self._save_state()
|
||||
|
||||
logger.log_state_change(
|
||||
"State Reset",
|
||||
old_state,
|
||||
default_state
|
||||
)
|
||||
|
||||
def add_csv_to_history(self, filepath: str, entry_count: int) -> None:
|
||||
"""
|
||||
Add a CSV file to the usage history
|
||||
"""
|
||||
if filepath not in self.csv_history:
|
||||
self.csv_history[filepath] = {
|
||||
"first_used": datetime.now().isoformat(),
|
||||
"last_used": datetime.now().isoformat(),
|
||||
"use_count": 1,
|
||||
"entry_count": entry_count
|
||||
}
|
||||
else:
|
||||
self.csv_history[filepath].update({
|
||||
"last_used": datetime.now().isoformat(),
|
||||
"use_count": self.csv_history[filepath]["use_count"] + 1,
|
||||
"entry_count": entry_count
|
||||
})
|
||||
|
||||
self._save_csv_history()
|
||||
logger.logger.debug(f"Added CSV to history: {filepath}")
|
||||
|
||||
def get_csv_history(self, limit: Optional[int] = None) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Get CSV usage history, optionally limited to the most recent entries
|
||||
"""
|
||||
if not limit:
|
||||
return self.csv_history
|
||||
|
||||
# Sort by last_used date and return limited number
|
||||
sorted_history = dict(
|
||||
sorted(
|
||||
self.csv_history.items(),
|
||||
key=lambda x: x[1]["last_used"],
|
||||
reverse=True
|
||||
)[:limit]
|
||||
)
|
||||
return sorted_history
|
||||
|
||||
def clear_csv_history(self) -> None:
|
||||
"""
|
||||
Clear the CSV usage history
|
||||
"""
|
||||
old_history = self.csv_history.copy()
|
||||
self.csv_history = {}
|
||||
self._save_csv_history()
|
||||
|
||||
logger.log_state_change(
|
||||
"CSV History Clear",
|
||||
old_history,
|
||||
{}
|
||||
)
|
||||
|
||||
# Singleton instance
|
||||
state_manager = StateManager()
|
||||
Reference in New Issue
Block a user