Files
Isulion-ComfyUI_Isulion/Docs/Theme_Creation_Guide.md
T

8.3 KiB

Theme Creation Guide

This guide documents how to add a new theme to both the IsulionMegaPromptV3 (single theme) and Isulion Multiple Prompt Generator nodes.

Architecture Overview

Core_Nodes/
├── mega_prompt_V3.py               # Node + ThemeRegistry + HANDLER_CLASS_NAMES_MAP
├── mega_prompt_all_themes.py        # Multiple Prompt Generator (theme categories)
├── configs/
│   └── <theme>_config.json          # Data lists (characters, locations, styles, effects...)
└── theme_handlers/
    ├── __init__.py                  # Exports all handler classes
    ├── base_handler.py              # BaseThemeHandler (abstract)
    └── <theme>_handler.py           # Handler logic

Step-by-step: Add theme my_theme

You must touch 4 files and optionally a 5th for the Multiple Prompt Generator.


1. Create the JSON config file

File: Core_Nodes/configs/my_theme_config.json

The top-level key must match the internal name (snake_case).

{
    "my_theme": {
        "characters": ["knight", "wizard", "dragon"],
        "outfits": ["plate armor", "robe", "scales"],
        "poses": ["battle stance", "casting spell"],
        "expressions": ["determined", "mysterious"],
        "locations": ["castle", "enchanted forest"],
        "styles": ["gothic", "medieval fantasy"],
        "effects": ["magic aura", "dragon fire"],
        "lighting": ["dramatic sunset", "torchlight"],
        "times": ["golden hour", "midnight"],
        "weather": ["clear sky", "misty"],
        "moods": ["epic", "whimsical"]
    }
}

Any category keys work — the handler reads them with dot notation: "my_theme.characters".


2. Create the handler class

File: Core_Nodes/theme_handlers/my_theme_handler.py

Inherit BaseThemeHandler and implement generate(). Use self._get_random_choice("my_theme.<key>") to pull from your config.

from typing import Dict
from .base_handler import BaseThemeHandler


class MyThemeHandler(BaseThemeHandler):
    """Handler for My Theme."""

    def generate(
        self,
        custom_subject: str = "",
        custom_location: str = "",
        include_environment: bool = True,
        include_style: bool = True,
        include_effects: bool = True,
    ) -> Dict[str, str]:
        components = {}

        character = self._get_random_choice("my_theme.characters")
        outfit = self._get_random_choice("my_theme.outfits")
        pose = self._get_random_choice("my_theme.poses")
        expression = self._get_random_choice("my_theme.expressions")

        if custom_subject:
            components["subject"] = (
                f"((masterful {character} rendition)) of {custom_subject}, "
                f"((wearing {outfit})), ((in {pose})), "
                f"((with {expression} expression))"
            )
        else:
            components["subject"] = (
                f"((masterful {character})) wearing ((detailed {outfit})), "
                f"((in {pose})), ((with {expression} expression))"
            )

        if include_environment:
            if custom_location:
                components["environment"] = (
                    f"in ((beautiful {custom_location})), "
                    f"((detailed environment))"
                )
            else:
                location = self._get_random_choice("my_theme.locations")
                time = self._get_random_choice("my_theme.times")
                weather = self._get_random_choice("my_theme.weather")
                components["environment"] = (
                    f"in ((stunning {location})) during (({time})) "
                    f"with (({weather}))"
                )

        if include_style:
            style = self._get_random_choice("my_theme.styles")
            components["style"] = (
                f"((masterfully rendered in {style} style)), "
                f"((professional composition)), 8k resolution"
            )

        if include_effects:
            effect = self._get_random_choice("my_theme.effects")
            mood = self._get_random_choice("my_theme.moods")
            lighting = self._get_random_choice("my_theme.lighting")
            components["effects"] = (
                f"with (({effect})), (({mood} atmosphere)), "
                f"((beautiful {lighting}))"
            )

        return components

Contract: generate() must return a Dict[str, str] with keys subject, environment, style, effects.


3. Register the handler in theme_handlers/__init__.py

Add an import and include the class name in __all__:

# Import (group with similar themes)
from .my_theme_handler import MyThemeHandler

# In __all__ list
__all__ = [
    # ...
    'MyThemeHandler',
    # ...
]

4. Register in mega_prompt_V3.py — two places

File: Core_Nodes/mega_prompt_V3.py

4a. HANDLER_CLASS_NAMES_MAP (line ~84) — maps internal name → handler class name string:

HANDLER_CLASS_NAMES_MAP = {
    # ...
    "my_theme": "MyThemeHandler",
    # ...
}

4b. _init_mappings() (line ~210) — maps display name with emoji → internal name:

all_emoji_mappings = {
    # ...
    "⚔️ My Theme": "my_theme",
    # ...
}

The display name (with emoji) is what users see in the ComfyUI dropdown. The internal name links to the handler and config.


5. (Optional) Add to Isulion Multiple Prompt Generator

File: Core_Nodes/mega_prompt_all_themes.py

Add your display name to the appropriate category in theme_categories:

theme_categories = {
    # ...
    "Science Fiction & Fantasy": [
        # ...
        "⚔️ My Theme",    # <-- add here
        # ...
    ],
    # ...
}

The node automatically picks it up from there. If your theme doesn't fit an existing category, you can create a new one.


Summary checklist

# File Action
1 Core_Nodes/configs/my_theme_config.json Create JSON with data lists
2 Core_Nodes/theme_handlers/my_theme_handler.py Create handler class extending BaseThemeHandler
3 Core_Nodes/theme_handlers/__init__.py Add import + __all__ entry
4 Core_Nodes/mega_prompt_V3.py Add to HANDLER_CLASS_NAMES_MAP + all_emoji_mappings
5 Core_Nodes/mega_prompt_all_themes.py Add to theme_categories category list

After making these changes, restart ComfyUI.

How the config manager loads data

ConfigManager (Core_Nodes/configs/config_manager.py) reads all *.json files from Core_Nodes/configs/ on startup and merges them into a single self.configs dict. The top-level key of each JSON file becomes the lookup key. Your config file can be named anything — my_theme_config.json — as long as its top-level key matches the internal name.

A handler calls self._get_random_choice("my_theme.characters") which resolves to self.config.get_config("my_theme.characters") → self.configs["my_theme"]["characters"].

Naming conventions

What Convention Example
Internal name snake_case my_theme
Handler class PascalCase + ThemeHandler suffix MyThemeHandler
Config file snake_case_config.json my_theme_config.json
Display name Emoji + Title Case ⚔️ My Theme
Config JSON key Same as internal name "my_theme": { ... }
Handler filename snake_case_handler.py my_theme_handler.py

Available helper methods on BaseThemeHandler

Method Purpose
_get_random_choice(key) Pick one random item from the config list at "theme.key"
_get_safe_random_choice(key, default) Same but returns default if not found
_get_random_choices(key, count) Pick count random items (with repeats if needed)

Troubleshooting

  • "Handler not found" → check that HANDLER_CLASS_NAMES_MAP value matches the class name exactly, and the class is exported in theme_handlers/__init__.py
  • "Config key not found" → verify the top-level key in your JSON matches the internal name, and the JSON is valid (no trailing commas)
  • Theme not in dropdown → check that internal name in all_emoji_mappings matches a key in HANDLER_CLASS_NAMES_MAP
  • Theme not in Multiple Prompt Generator → verify the display name in theme_categories matches the display name in all_emoji_mappings exactly