Introduces a production-ready, schema-driven prompt generation engine for ComfyUI, including a new 'everyday_casual' library with 290+ atomic options. Major updates include a comprehensive README, scaling and library comparison docs, a strict LICENSE (CC BY-NC 4.0), and a refactored __init__.py with improved node structure, negative prompt support, and detailed docstrings. The engine (engine/composer.py) now enforces strict separation of concerns, supports addons, intent flags, and deterministic assembly. Documentation and changelog updated to reflect the new scalable, maintainable architecture.
222 lines
7.1 KiB
Python
222 lines
7.1 KiB
Python
"""
|
|
Mode_personal_node: Schema-driven prompt generation engine for ComfyUI.
|
|
|
|
Licensed under CC BY-NC 4.0.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
node_path = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, node_path)
|
|
|
|
from engine.composer import Composer
|
|
|
|
SCHEMA = os.path.join(node_path, "schema", "connector.json")
|
|
LIBS = os.path.join(node_path, "libraries")
|
|
|
|
|
|
class PromptComposerNode:
|
|
"""
|
|
Structured prompt composition node for diffusion models.
|
|
|
|
Provides dropdown-based selection interface for schema-defined blocks,
|
|
assembling prompts according to model-specific ordering constraints.
|
|
Each block supports an optional addon string for inline modification.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.composer = Composer(SCHEMA, LIBS)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
"""Define node input structure."""
|
|
inst = cls()
|
|
c = inst.composer
|
|
|
|
libs = c.get_lib_names()
|
|
if not libs:
|
|
libs = ["default"]
|
|
|
|
default_lib = libs[0]
|
|
order = c.schema["assembly"]["order"]
|
|
|
|
# Start with base inputs
|
|
inputs = {
|
|
"required": {},
|
|
"optional": {}
|
|
}
|
|
|
|
# Add library and model first
|
|
inputs["required"]["library"] = (libs,)
|
|
inputs["required"]["model"] = (["sdxl", "flux"], {"default": "sdxl"})
|
|
|
|
# Add each block dropdown (NO addons yet - we'll add them after)
|
|
for block in order:
|
|
opts = c.get_options(default_lib, block, include_none=True)
|
|
inputs["required"][block] = (opts,)
|
|
|
|
# Now add ALL addons as optional at the end
|
|
for block in order:
|
|
addon_key = f"{block}_addon"
|
|
inputs["optional"][addon_key] = ("STRING", {
|
|
"default": "",
|
|
"multiline": False
|
|
})
|
|
|
|
# Add global custom text input
|
|
inputs["optional"]["custom"] = ("STRING", {
|
|
"multiline": True,
|
|
"default": ""
|
|
})
|
|
|
|
return inputs
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("prompt",)
|
|
FUNCTION = "compose_prompt"
|
|
CATEGORY = "prompting/structured"
|
|
|
|
def compose_prompt(self, library, model, custom="", **kwargs):
|
|
"""
|
|
Compose prompt from selections and addons.
|
|
|
|
Args:
|
|
library: Selected library name
|
|
model: Target model (sdxl/flux)
|
|
custom: Optional custom text to append
|
|
**kwargs: Block selections and addon strings
|
|
|
|
Returns:
|
|
Tuple containing assembled prompt string
|
|
"""
|
|
# DEBUG: Print all received arguments
|
|
print("\n=== DEBUG: compose_prompt called ===")
|
|
print(f"Library: {library}")
|
|
print(f"Model: {model}")
|
|
print(f"Custom: {custom}")
|
|
print(f"All kwargs keys: {list(kwargs.keys())}")
|
|
print(f"All kwargs: {kwargs}")
|
|
|
|
order = self.composer.schema["assembly"]["order"]
|
|
|
|
# Separate selections from addons
|
|
selections = {}
|
|
addons = {}
|
|
|
|
for key, value in kwargs.items():
|
|
if key.endswith("_addon"):
|
|
# This is an addon string
|
|
block_name = key.replace("_addon", "")
|
|
if value and value.strip():
|
|
addons[block_name] = value.strip()
|
|
print(f"Addon: {block_name} = '{value.strip()}'")
|
|
else:
|
|
# This is a block selection
|
|
if value and value != "None":
|
|
selections[key] = value
|
|
print(f"Selection: {key} = '{value}'")
|
|
|
|
print(f"\nFinal selections: {selections}")
|
|
print(f"Final addons: {addons}")
|
|
|
|
# Assemble prompt with addons
|
|
prompt = self.composer.compose(library, selections, model, addons=addons)
|
|
|
|
print(f"\nGenerated prompt: {prompt}")
|
|
print("=== END DEBUG ===\n")
|
|
|
|
# Append global custom text if provided
|
|
if custom.strip():
|
|
prompt += ", BREAK, " + custom.strip()
|
|
|
|
return (prompt,)
|
|
|
|
|
|
class NegativePromptNode:
|
|
"""
|
|
Configurable negative prompt generator.
|
|
|
|
Provides boolean toggles for common negative prompt categories,
|
|
with optional custom text addition.
|
|
"""
|
|
|
|
CATEGORIES = {
|
|
"quality": "blurry, low quality, jpeg artifacts, watermark, signature",
|
|
"anatomy": "deformed face, asymmetrical eyes, bad proportions, extra limbs, merged fingers",
|
|
"style": "cartoon, anime, 3d render, illustration, artificial",
|
|
"lighting": "harsh shadows, overexposed, underexposed, bad lighting",
|
|
"context": "multiple people, cluttered background, busy composition",
|
|
"texture": "duplicate, pixelated, grainy noise, compression artifacts"
|
|
}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
"""Define node input structure."""
|
|
return {
|
|
"required": {
|
|
"quality": ("BOOLEAN", {"default": True}),
|
|
"anatomy": ("BOOLEAN", {"default": True}),
|
|
"style": ("BOOLEAN", {"default": True}),
|
|
"lighting": ("BOOLEAN", {"default": False}),
|
|
"context": ("BOOLEAN", {"default": False}),
|
|
"texture": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"custom": ("STRING", {"multiline": True, "default": ""})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("negative",)
|
|
FUNCTION = "compose_negative"
|
|
CATEGORY = "prompting/structured"
|
|
|
|
def compose_negative(self, quality, anatomy, style, lighting, context, texture, custom=""):
|
|
"""
|
|
Compose negative prompt from selected categories.
|
|
|
|
Args:
|
|
quality: Include quality-related negatives
|
|
anatomy: Include anatomy-related negatives
|
|
style: Include style-related negatives
|
|
lighting: Include lighting-related negatives
|
|
context: Include context-related negatives
|
|
texture: Include texture-related negatives
|
|
custom: Optional custom negative text
|
|
|
|
Returns:
|
|
Tuple containing assembled negative prompt string
|
|
"""
|
|
parts = []
|
|
|
|
if quality:
|
|
parts.append(self.CATEGORIES["quality"])
|
|
if anatomy:
|
|
parts.append(self.CATEGORIES["anatomy"])
|
|
if style:
|
|
parts.append(self.CATEGORIES["style"])
|
|
if lighting:
|
|
parts.append(self.CATEGORIES["lighting"])
|
|
if context:
|
|
parts.append(self.CATEGORIES["context"])
|
|
if texture:
|
|
parts.append(self.CATEGORIES["texture"])
|
|
|
|
if custom.strip():
|
|
parts.append(custom.strip())
|
|
|
|
return (", ".join(parts),)
|
|
|
|
|
|
# ComfyUI node registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"PromptComposerNode": PromptComposerNode,
|
|
"NegativePromptNode": NegativePromptNode
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"PromptComposerNode": "Structured Prompt Composer",
|
|
"NegativePromptNode": "Structured Negative Prompt"
|
|
}
|