8.6 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 (reads themes from the handler registry)
├── mega_prompt_all_themes.py # Multiple Prompt Generator (theme categories)
├── configs/
│ └── <theme>_config.json # Data lists (characters, locations, styles, effects...)
└── theme_handlers/
├── __init__.py # Package init
├── registry.py # @register_theme decorator (THEME_HANDLERS registry)
├── base_handler.py # BaseThemeHandler (abstract)
└── <theme>_handler.py # Handler logic (auto-discovered & registered)
Step-by-step: Add theme my_theme
You only need to create 2 files and add one line to a third for the Multiple Prompt Generator. Nothing else needs to be registered — auto-discovery handles the rest.
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, decorate the class with @register_theme("my_theme"), and set a DISPLAY_NAME attribute (emoji + title, exactly as it should appear in the ComfyUI dropdown). Implement generate(), using self._get_random_choice("my_theme.<key>") to pull from your config.
from typing import Dict
from .base_handler import BaseThemeHandler
from .registry import register_theme
@register_theme("my_theme")
class MyThemeHandler(BaseThemeHandler):
"""Handler for My Theme."""
DISPLAY_NAME = "⚔️ 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. Done — nothing else to register
The @register_theme("my_theme") decorator adds your handler to the central registry (theme_handlers/registry.py) as soon as the module is loaded. Auto-discovery picks up new handler files automatically:
- No edits to
theme_handlers/__init__.py - No edits to
mega_prompt_V3.py(no handler-name map or emoji-mapping edits)
Your theme appears in the IsulionMegaPromptV3 dropdown under its DISPLAY_NAME, linked to the internal name you registered.
4. Add to Isulion Multiple Prompt Generator
File: Core_Nodes/mega_prompt_all_themes.py
Add your display name to exactly one category in theme_categories. A theme left out of every category cannot be generated by the Multiple Prompt Generator, not even in "All Themes" mode:
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 with @register_theme("my_theme") + DISPLAY_NAME |
| 3 | Core_Nodes/mega_prompt_all_themes.py |
Add the display name to one theme_categories 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, in alphabetical order, and merges them into a single self.configs dict. Top-level keys must be unique across files: a duplicate overrides the earlier file and logs a warning. 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
- "Duplicate theme registration" → another handler is already registered under your internal name; pick a unique one
- "Handler not found" / handler never loads → verify the module is importable (no syntax errors) and that
@register_theme("my_theme")uses the same internal name as your config JSON top-level key - "Config key not found" → verify the top-level key in your JSON matches the internal name passed to
@register_theme, and the JSON is valid (no trailing commas) - Theme not in dropdown → check that
DISPLAY_NAMEis set on the handler class - Theme not in Multiple Prompt Generator → add its display name to a category in
theme_categories(mega_prompt_all_themes.py)