Files
Isulion-ComfyUI_Isulion/Core_Nodes/mega_prompt_V3.py
T
2026-07-29 20:21:57 +02:00

555 lines
26 KiB
Python

import random
import os
from typing import Dict, List, Tuple, Optional
import json
# Config imports - Reverting to the original import that you said was working
# This suggests that ComfyUI's loading context makes '.configs' resolve correctly.
try:
from .configs.config_manager import ConfigManager
except ImportError as e:
print(f"CRITICAL ERROR: Could not import ConfigManager from '.configs.config_manager'. Please check the file path and your __init__.py. Error: {e}")
# Define a dummy ConfigManager to prevent immediate crash, but node won't work.
class ConfigManager:
def __init__(self):
print("Warning: Using dummy ConfigManager. Node will not function correctly.")
self.random = random # Fallback to standard random, won't be seeded by ComfyUI
def set_seed(self, seed):
print(f"Warning: Dummy ConfigManager: set_seed called with {seed}")
self.random.seed(seed)
def get_current_seed(self):
print("Warning: Dummy ConfigManager: get_current_seed called.")
# Standard random doesn't expose current state easily, return dummy
return 0
def _load_configs(self):
current_dir = os.path.dirname(os.path.abspath(__file__))
print(f"[DEBUG] ConfigManager._load_configs: current_dir={current_dir}")
self.configs = {}
found_json = False
for filename in os.listdir(current_dir):
if filename.endswith('.json'):
found_json = True
file_path = os.path.join(current_dir, filename)
print(f"[DEBUG] Loading config file: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
self.configs.update(json.load(f))
except Exception as e:
print(f"[ERROR] Error loading config file {filename}: {str(e)}")
if not found_json:
print("[WARNING] No .json config files found in config directory!")
if not self.configs:
print("[WARNING] No configs loaded in ConfigManager!")
# Import the base handler class.
# Based on your tree, this should be in theme_handlers/base_handler.py
# This was missing from the original snippet you provided, causing the NameError.
try:
from .theme_handlers.base_handler import BaseThemeHandler
# print("Successfully imported BaseThemeHandler") # Debugging
except ImportError as e:
print(f"CRITICAL ERROR: Could not import BaseThemeHandler from '.theme_handlers.base_handler'. Ensure the file exists and defines BaseThemeHandler. Error: {e}")
# Define a dummy BaseThemeHandler to allow the node to load, but generation will fail.
class BaseThemeHandler:
def __init__(self, config_manager=None):
self.config_manager = config_manager or ConfigManager() # Use dummy ConfigManager if needed
self.debug = False
def set_debug(self, debug: bool):
self.debug = debug
def generate(self, custom_subject: str, custom_location: str, include_environment: bool, include_style: bool, include_effects: bool) -> Dict[str, str]:
print("Warning: Using dummy BaseThemeHandler. Prompt generation will be empty.")
return {"subject": "Error: Dummy handler used", "environment": "", "style": "", "effects": ""}
# Import all theme handlers using the original pattern.
# This relies on your theme_handlers/__init__.py or directory structure
# correctly exposing the handler classes.
# This *might* be the source of the 'NameError' if the __init__py isn't right,
# but let's stick to the original structure first.
try:
# This import needs to make classes like AbstractThemeHandler, etc., available in the local scope
from .theme_handlers import *
print("Successfully imported theme handlers via '*'.") # Debugging
except ImportError as e:
print(f"CRITICAL WARNING: Could not import theme handlers using 'from .theme_handlers import *'. Theme selection may be limited or fail. Error: {e}")
# Continue loading, but warn the user. ThemeRegistry will have limited or no handlers.
# Now define the mapping from display name to internal name.
# We *must* use the class names here as strings, because the 'from ... import *'
# happens *before* this dictionary is defined, making the class names available.
# But the dictionary needs the *names*, not the classes themselves directly for lookup.
# We rely on ThemeRegistry._init_handlers to check if the class exists *after* import *.
HANDLER_CLASS_NAMES_MAP = {
# Using strings for class names now
"abstract": "AbstractThemeHandler",
"animation_cartoon": "AnimationCartoonThemeHandler",
"anime": "AnimeThemeHandler",
"architectural": "ArchitecturalThemeHandler",
"bio_organic_tech": "BioOrganicTechThemeHandler",
"binet_surreal": "BinetSurrealThemeHandler",
"caricature": "CaricatureThemeHandler",
"chimera_animals": "ChimeraAnimalsThemeHandler",
"chimera_cute_animals": "ChimeraCuteAnimalsThemeHandler",
"christmas": "ChristmasThemeHandler",
"cinema_studio": "CinemaStudioThemeHandler",
"clay_art": "ClayArtThemeHandler",
"comic_book": "ComicBookThemeHandler",
"character_designer": "CharacterDesignerThemeHandler",
"chinese_new_year": "ChineseNewYearThemeHandler",
"concept_art": "ConceptArtThemeHandler",
"crayon_art": "CrayonArtThemeHandler",
"crystalpunk": "CrystalpunkThemeHandler",
"cyberpunk": "CyberpunkThemeHandler",
"culinary_food": "CulinaryFoodThemeHandler",
"curvy_fashion": "CurvyFashionThemeHandler",
"dia_de_los_muertos": "DiaDeLosmuertosThemeHandler",
"digital_art": "DigitalArtThemeHandler",
"disney": "DisneyThemeHandler",
"dreamworks": "DreamworksThemeHandler",
"dimension_3d": "Dimension3DThemeHandler",
"easter": "EasterThemeHandler",
"enchanted_fantasy": "EnchantedFantasyThemeHandler",
"essential_realistic": "EssentialRealisticThemeHandler",
"essential_vintage": "EssentialVintageThemeHandler",
"ethereal_dreams": "EtherealDreamsThemeHandler",
"experimental_art": "ExperimentalArtThemeHandler",
"fantasy": "FantasyThemeHandler",
"fifties_commercial": "FiftiesCommercialHandler",
"futuristic_battlefield": "FuturisticBattlefieldThemeHandler",
"futuristic_city": "FuturisticCityThemeHandler",
"futuristic_city_metropolis": "FuturisticCityMetropolisThemeHandler",
"futuristic_scifi": "FuturisticSciFiThemeHandler",
"ghibli": "GhibliThemeHandler",
"halloween": "HalloweenThemeHandler",
"halloween_ethereal": "HalloweenEtherealThemeHandler",
"historical_monuments": "HistoricalMonumentsHandler",
"horror": "HorrorThemeHandler",
"impressionist": "ImpressionistThemeHandler",
"instagram": "InstagramThemeHandler",
"instagram_lifestyle": "InstagramLifestyleThemeHandler",
"interior_spaces": "InteriorSpacesThemeHandler",
"logo": "LogoThemeHandler",
"manga_panel": "MangaPanelThemeHandler",
"marvel": "MarvelThemeHandler",
"medieval": "MedievalThemeHandler",
"microscopic": "MicroscopicThemeHandler",
"minimalist": "MinimalistThemeHandler",
"miura": "MiuraThemeHandler",
"nature": "NatureThemeHandler",
"new_years_eve": "NewYearsEveThemeHandler",
"nolan": "NolanThemeHandler",
"peaky_blinders": "PeakyBlindersThemeHandler",
"pixar": "PixarThemeHandler",
"post_apocalyptic": "PostApocalypticThemeHandler",
"puzzle_dimension": "PuzzleDimensionHandler", # Corrected? Original had puzzle_dimension, tree has puzzle_dimension_handler
"scifi": "SciFiThemeHandler",
"school_manga": "SchoolMangaThemeHandler",
"selfie": "SelfieThemeHandler",
"skinny_blonde_girl": "SkinnyBlondeGirlHandler",
"Spartan_300": "s300ThemeHandler",
"spectral_mist": "SpectralMistThemeHandler",
"st_patricks_day": "StPatricksDayThemeHandler",
"star_wars": "StarWarsThemeHandler",
"steampunk": "SteampunkThemeHandler",
"stopmotion": "StopMotionHandler", # Corrected? Original had stopmotion, tree has stopmotion_handler
"street_food_kebab": "StreetFoodKebabThemeHandler",
"thanksgiving": "ThanksgivingThemeHandler",
"underwater_civilization": "UnderwaterCivilizationThemeHandler",
"urban_tag": "UrbanTagThemeHandler",
"valentines_day": "ValentinesDayThemeHandler",
"village_world": "VillageWorldThemeHandler",
"vintage_1800s_photography": "Vintage1800sPhotographyHandler",
"vintage_anthropomorphic": "VintageAnthropomorphicThemeHandler",
"watercolor": "WatercolorThemeHandler",
"starter_pack": "StarterPackThemeHandler",
"space_colony": "SpaceColonyHandler",
"tropical_paradise": "TropicalParadiseThemeHandler",
# Removed duplicates that were in your original list
}
class ThemeRegistry:
"""Registry for managing theme handlers and their mappings."""
def __init__(self, config_manager: ConfigManager, debug: bool = False):
self.config_manager = config_manager
self.debug = debug
self.handlers: Dict[str, BaseThemeHandler] = {}
self.theme_mappings: Dict[str, str] = {}
self._init_handlers()
self._init_mappings()
def _debug_print(self, msg):
if self.debug:
print(msg)
def _init_handlers(self):
self._debug_print("[DEBUG] ThemeRegistry._init_handlers called")
self.handlers = {}
for internal_name, class_name_str in HANDLER_CLASS_NAMES_MAP.items():
handler_class = globals().get(class_name_str)
if handler_class is None:
self._debug_print(f"[DEBUG] Handler class '{class_name_str}' for theme '{internal_name}' not found in global scope.")
continue
if 'BaseThemeHandler' in globals() and not issubclass(handler_class, BaseThemeHandler):
print(f"Warning: Loaded object '{class_name_str}' for theme '{internal_name}' is not a valid handler class (doesn't inherit BaseThemeHandler). Skipping.")
continue
try:
self.handlers[internal_name] = handler_class(self.config_manager)
self._debug_print(f"[DEBUG] Initialized handler for '{internal_name}' ({class_name_str})")
except Exception as e:
print(f"Warning: Error initializing handler '{class_name_str}' for theme '{internal_name}': {e}. Skipping.")
continue
if not self.handlers:
print("[WARNING] No theme handlers were initialized in ThemeRegistry!")
def _init_mappings(self):
self._debug_print("[DEBUG] ThemeRegistry._init_mappings called")
all_emoji_mappings = {
"🎲 Dynamic Random": "random",
"🎨 Abstract": "abstract",
"📺 Animation Cartoon": "animation_cartoon",
"🎌 Anime": "anime",
"🏛️ Architectural": "architectural",
"🖼️ Binet Surreal": "binet_surreal",
"🧬 Bio-Organic Technology": "bio_organic_tech",
"😄 Caricature": "caricature",
"👤 Character Designer": "character_designer",
"🦄 Chimera Animals": "chimera_animals",
"🐰 Chimera Cute Animals": "chimera_cute_animals",
"🏮 Chinese New Year": "chinese_new_year",
"🎄 Christmas": "christmas",
"🎬 Cinema Studio": "cinema_studio",
"🏺 Clay Art": "clay_art",
"📚 Comic Book": "comic_book",
"🎨 Concept Art": "concept_art",
"🖍️ Crayon Art": "crayon_art",
"💎 Crystalpunk": "crystalpunk",
"🍳 Culinary/Food": "culinary_food",
"👗 Curvy Fashion": "curvy_fashion",
"🌆 Cyberpunk": "cyberpunk",
"👹 Dia de los Muertos": "dia_de_los_muertos",
"🖼️ Digital Art": "digital_art",
"💠 Dimension 3D": "dimension_3d",
"🎡 Disney": "disney",
"🎬 Dreamworks": "dreamworks",
"🐰 Easter": "easter",
"✨ Enchanted Fantasy": "enchanted_fantasy",
"📸 Essential Realistic": "essential_realistic",
"🕰️ Essential Vintage": "essential_vintage",
"✨ Ethereal Dreams": "ethereal_dreams",
"🔬 Experimental Art": "experimental_art",
"⚔️ Fantasy": "fantasy",
"🧺 50s Commercial": "fifties_commercial",
"⚔️ Futuristic Battlefield": "futuristic_battlefield",
"🌆 Futuristic City": "futuristic_city",
"🌆 Futuristic City Metropolis": "futuristic_city_metropolis",
"🚀 Futuristic Sci-Fi": "futuristic_scifi",
"🍃 Ghibli": "ghibli",
"🎃 Halloween": "halloween",
"👻 Halloween Ethereal": "halloween_ethereal",
"🏛️ Historical Monuments": "historical_monuments",
"👻 Horror": "horror",
"🎨 Impressionist": "impressionist",
"📱 Instagram": "instagram",
"📱 Instagram Lifestyle": "instagram_lifestyle",
"🏠 Interior Spaces": "interior_spaces",
"🎯 Logo": "logo",
"📺 Manga Panel": "manga_panel",
"🦸 Marvel": "marvel",
"⚔️ Medieval": "medieval",
"🔬 Microscopic": "microscopic",
"⬜ Minimalist": "minimalist",
"⚔️ Miura Dark Fantasy": "miura",
"🌿 Nature": "nature",
"🎆 New Year's Eve": "new_years_eve",
"🎬 Nolan Epic": "nolan",
"🕴️ Peaky Blinders": "peaky_blinders",
"💫 Pixar": "pixar",
"🌪️ Post Apocalyptic": "post_apocalyptic",
"🧩 Puzzle Dimension": "puzzle_dimension",
"🚀 Sci-Fi": "scifi",
"📚 School Manga": "school_manga",
"📱 Selfie": "selfie",
"👧 Skinny Blonde Girl": "skinny_blonde_girl",
"🏛️ Spartan 300": "Spartan_300",
"💗 Spectral Mist": "spectral_mist",
"🍀 St. Patrick's Day": "st_patricks_day",
"🚀 Star Wars": "star_wars",
"⚙️ Steampunk": "steampunk",
"🎭 Stop Motion": "stopmotion",
"🥙 Street Food Kebab": "street_food_kebab",
"🦃 Thanksgiving": "thanksgiving",
"🌊 Underwater Civilization": "underwater_civilization",
"🏙️ Urban Tag": "urban_tag",
"💘 Valentine's Day": "valentines_day",
"🏠 Village World": "village_world",
"📸 Vintage 1800s Photography": "vintage_1800s_photography",
"👴 Vintage Anthropomorphic": "vintage_anthropomorphic",
"🎨 Watercolor": "watercolor",
"🧸 Starter Pack": "starter_pack",
"🚀 Space Colony": "space_colony",
"🏝️ Tropical Paradise": "tropical_paradise"
}
available_internal_names = set(self.handlers.keys()).union({"random"})
self.theme_mappings = {
display_name: internal_name
for display_name, internal_name in all_emoji_mappings.items()
if internal_name in available_internal_names
}
self._debug_print(f"[DEBUG] theme_mappings after filtering: {list(self.theme_mappings.keys())}")
def get_handler(self, theme: str) -> Optional[BaseThemeHandler]:
return self.handlers.get(theme)
def get_internal_theme(self, display_theme: str) -> str:
internal_name = self.theme_mappings.get(display_theme)
if internal_name is None:
print(f"Warning: Display theme '{display_theme}' not found in available mappings. Using 'random'.")
return "random"
return internal_name
def get_random_theme(self) -> str:
available_themes = list(self.handlers.keys())
if not available_themes:
print("Error: No theme handler instances available for random selection.")
if "essential_realistic" in self.handlers:
print("Falling back to 'essential_realistic'.")
return "essential_realistic"
else:
raise ValueError("Cannot select random theme: No handler instances were initialized and no fallback available.")
return self.config_manager.random.choice(available_themes)
def get_all_display_themes(self) -> List[str]:
available_display_themes = list(self.theme_mappings.keys())
dynamic_random = "🎲 Dynamic Random"
if dynamic_random in available_display_themes:
available_display_themes.remove(dynamic_random)
available_display_themes.sort(key=lambda item: item.split(' ', 1)[-1].strip())
available_display_themes.insert(0, dynamic_random)
else:
available_display_themes.sort(key=lambda item: item.split(' ', 1)[-1].strip())
return available_display_themes
def deduplicate_prompt_parts(parts):
"""Remove duplicate prompt parts while preserving order and ignoring case/whitespace."""
seen = set()
result = []
for part in parts:
norm = part.strip().lower()
if norm and norm not in seen:
seen.add(norm)
result.append(part)
return result
class IsulionMegaPromptV3:
theme_mappings = {}
TITLE = "🚀 Mega Prompt V3"
def __init__(self):
try:
self.debug_mode = False
self.config_manager = ConfigManager()
self.theme_registry = ThemeRegistry(self.config_manager, debug=self.debug_mode)
self.theme_mappings = self.theme_registry.theme_mappings
if self.debug_mode:
print(f"[DEBUG] theme_mappings set on instance: {list(self.theme_mappings.keys())}")
if not self.theme_registry.handlers and "random" not in self.theme_registry.theme_mappings.values():
print("CRITICAL WARNING: No theme handler instances were successfully loaded and 'random' option is unavailable. The node will likely fail to generate prompts.")
except Exception as e:
print(f"[ERROR] Exception in IsulionMegaPromptV3.__init__: {type(e).__name__}: {e}")
self.theme_mappings = {}
self.theme_registry = None
@classmethod
def INPUT_TYPES(cls) -> Dict:
debug_mode = False
try:
if debug_mode:
print("[DEBUG] IsulionMegaPromptV3.INPUT_TYPES called")
temp_registry = ThemeRegistry(ConfigManager(), debug=debug_mode)
if debug_mode:
print(f"[DEBUG] temp_registry.theme_mappings: {list(temp_registry.theme_mappings.keys())}")
available_themes = temp_registry.get_all_display_themes()
if debug_mode:
print(f"[DEBUG] available_themes for dropdown: {available_themes}")
except Exception as e:
print(f"[ERROR] Exception in INPUT_TYPES: {type(e).__name__}: {e}")
available_themes = ["--- Error ---"]
default_theme = "🎲 Dynamic Random" if "🎲 Dynamic Random" in available_themes else (available_themes[0] if available_themes else "--- Error ---")
if not available_themes:
available_themes = ["--- Error ---"]
default_theme = "--- Error ---"
print("Error: No themes available for INPUT_TYPES dropdown.")
return {
"required": {
"theme": (available_themes, {"default": default_theme}),
"complexity": (["simple", "detailed", "complex"], {"default": "detailed"}),
"randomize": (["enable", "disable"], {"default": "enable"}),
"debug_mode": (["off", "on"], {"default": "off"}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"custom_subject": ("STRING", {"default": "", "multiline": True}),
"custom_location": ("STRING", {"default": "", "multiline": True}),
"lora_key": ("STRING", {"default": "", "multiline": True}),
"include_environment": (["yes", "no"], {"default": "yes"}),
"include_style": (["yes", "no"], {"default": "yes"}),
"include_effects": (["yes", "no"], {"default": "yes"}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "INT")
RETURN_NAMES = ("prompt", "selected_theme", "subject", "environment", "style", "effects", "seed")
FUNCTION = "generate"
CATEGORY = "Isulion/Core"
def generate(self, theme: str, complexity: str = "detailed", randomize: str = "enable",
seed: int = 0, custom_subject: str = "", custom_location: str = "",
lora_key: str = "",
include_environment: str = "yes", include_style: str = "yes",
include_effects: str = "yes", debug_mode: str = "off") -> Tuple[str, str, str, str, str, str, int]:
self.debug_mode = (debug_mode == "on")
if self.debug_mode:
print(f"[DEBUG] generate called with theme={theme}, seed={seed}")
return_seed = seed
if theme == "--- Error ---":
error_msg = "Prompt generation failed: Theme handlers could not be loaded during startup."
print(error_msg)
return (error_msg, theme, "", "", "", "", return_seed)
try:
self.config_manager.set_seed(seed)
internal_theme = self.theme_registry.get_internal_theme(theme)
# Track the display name of the selected theme for output
selected_theme_display = theme
if internal_theme == "random":
try:
internal_theme = self.theme_registry.get_random_theme()
# Find the display name for the randomly selected internal theme
for display_name, internal_name in self.theme_registry.theme_mappings.items():
if internal_name == internal_theme:
selected_theme_display = display_name
break
else:
selected_theme_display = internal_theme # fallback
except ValueError as e:
error_msg = f"Error selecting random theme: {e}"
print(error_msg)
return (
f"Error: {error_msg}",
"random",
"", "", "", "",
return_seed
)
else:
# Always show the display name for the selected theme
for display_name, internal_name in self.theme_registry.theme_mappings.items():
if internal_name == internal_theme:
selected_theme_display = display_name
break
else:
selected_theme_display = internal_theme # fallback
handler = self.theme_registry.get_handler(internal_theme)
if self.debug_mode:
print(f"[DEBUG] Got handler for internal_theme={internal_theme}: {handler}")
if not handler:
error_msg = f"Error: Handler instance for theme '{internal_theme}' (selected via '{theme}') could not be found or was not initialized."
print(error_msg)
return (
error_msg, selected_theme_display, "", "", "", "", return_seed
)
handler.set_debug(debug_mode == "on")
components = handler.generate(
custom_subject=custom_subject,
custom_location=custom_location,
include_environment=(include_environment == "yes"),
include_style=(include_style == "yes"),
include_effects=(include_effects == "yes")
)
if self.debug_mode:
print(f"[DEBUG] Handler returned components: {components}")
if not isinstance(components, dict):
raise TypeError(f"Handler for theme '{internal_theme}' returned invalid type: {type(components)}. Expected dict.")
prompt_parts = []
subject = components.get("subject", "")
if subject and isinstance(subject, str):
prompt_parts.append(subject)
elif subject is not None:
print(f"Warning: Handler for theme '{internal_theme}' generated subject of type {type(subject)}. Expected string.")
if include_environment:
environment = components.get("environment", "")
if self.debug_mode:
print(f"[DEBUG] environment type: {type(environment)}, value: {environment!r}")
if isinstance(environment, str):
if environment:
prompt_parts.append(environment)
if include_style:
style = components.get("style", "")
if self.debug_mode:
print(f"[DEBUG] style type: {type(style)}, value: {style!r}")
if isinstance(style, str):
if style:
prompt_parts.append(style)
if include_effects:
effects = components.get("effects", "")
if self.debug_mode:
print(f"[DEBUG] effects type: {type(effects)}, value: {effects!r}")
if isinstance(effects, str):
if effects:
prompt_parts.append(effects)
if lora_key and lora_key.strip():
prompt_parts.append(lora_key.strip())
# Deduplicate prompt parts globally before joining
prompt_parts = deduplicate_prompt_parts(prompt_parts)
final_prompt = ", ".join(filter(None, prompt_parts))
return (
final_prompt,
selected_theme_display,
components.get("subject", "") if isinstance(components.get("subject"), str) else "",
components.get("environment", "") if include_environment and isinstance(components.get("environment"), str) else "",
components.get("style", "") if include_style and isinstance(components.get("style"), str) else "",
components.get("effects", "") if include_effects and isinstance(components.get("effects"), str) else "",
return_seed
)
except Exception as e:
error_msg = f"Error during prompt generation for theme '{theme}' (internal '{internal_theme if 'internal_theme' in locals() else 'N/A'}'): {type(e).__name__}: {str(e)}"
print(error_msg)
return (
f"Error: {error_msg}",
theme,
"", "", "", "",
return_seed
)