diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0064e80 --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index ba0430d..eb2ef2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -__pycache__/ \ No newline at end of file +__pycache__/ +To-Do.md +logs/ +config/animagine_state.json +config/csv_history.json +logs/animagine_prompt_20250130.log diff --git a/README.md b/README.md index a0dc27d..e19c3bf 100644 --- a/README.md +++ b/README.md @@ -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. ![Node Preview](node_preview.png) + +## 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 \ No newline at end of file diff --git a/__init__.py b/__init__.py index f047a7b..5f1a7c5 100644 --- a/__init__.py +++ b/__init__.py @@ -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" \ No newline at end of file +__version__ = "1.0.1" \ No newline at end of file diff --git a/animagine_node.py b/animagine_node.py index c925d1d..d65d4ba 100644 --- a/animagine_node.py +++ b/animagine_node.py @@ -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 \ No newline at end of file diff --git a/csv_handler.py b/csv_handler.py index e605309..ed915b8 100644 --- a/csv_handler.py +++ b/csv_handler.py @@ -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 \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..4b0dfeb --- /dev/null +++ b/logger.py @@ -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() \ No newline at end of file diff --git a/logs/animagine_prompt_20250130.log b/logs/animagine_prompt_20250130.log new file mode 100644 index 0000000..4c1ecf6 --- /dev/null +++ b/logs/animagine_prompt_20250130.log @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 71f7381..8dd7b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "" diff --git a/state_manager.py b/state_manager.py new file mode 100644 index 0000000..0680958 --- /dev/null +++ b/state_manager.py @@ -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() \ No newline at end of file