Introduces a scalable, production-grade prompt composition system for ComfyUI. Adds pure data libraries, schema-driven rules, a content-agnostic engine, and minimal node interface. Includes documentation (ARCHITECTURE.md, MIGRATION.md, QUICKSTART.md, README.md), three example libraries, and schema configuration for block order, weights, and model profiles.
93 lines
3.0 KiB
Python
93 lines
3.0 KiB
Python
"""
|
|
Pure composition engine - content agnostic
|
|
"""
|
|
import json
|
|
import os
|
|
import glob
|
|
|
|
class PromptComposer:
|
|
"""
|
|
Generic prompt composition engine.
|
|
Knows nothing about content, only follows schema rules.
|
|
"""
|
|
|
|
def __init__(self, schema_path, libraries_path):
|
|
self.schema = self._load_json(schema_path)
|
|
self.libraries = self._load_libraries(libraries_path)
|
|
|
|
def _load_json(self, path):
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
|
|
def _load_libraries(self, path):
|
|
libraries = {}
|
|
for json_file in glob.glob(os.path.join(path, "*.json")):
|
|
lib_id = os.path.splitext(os.path.basename(json_file))[0]
|
|
libraries[lib_id] = self._load_json(json_file)
|
|
return libraries
|
|
|
|
def get_library_names(self):
|
|
return list(self.libraries.keys())
|
|
|
|
def get_block_options(self, library_id, block_name):
|
|
if library_id not in self.libraries:
|
|
return []
|
|
blocks = self.libraries[library_id].get("blocks", {})
|
|
return list(blocks.get(block_name, {}).keys())
|
|
|
|
def apply_weight(self, text, weight, model_profile):
|
|
model_config = self.schema["models"].get(model_profile, {})
|
|
if not model_config.get("enable_weights", False):
|
|
return text
|
|
if weight == 1.0:
|
|
return text
|
|
|
|
adjusted_weight = weight * model_config.get("weight_multiplier", 1.0)
|
|
weight_format = model_config.get("weight_format", "({text}:{weight})")
|
|
|
|
return weight_format.format(text=text, weight=f"{adjusted_weight:.1f}")
|
|
|
|
def compose(self, library_id, selections, model_profile="sdxl"):
|
|
"""
|
|
Compose final prompt from selections.
|
|
|
|
Args:
|
|
library_id: Selected library
|
|
selections: Dict of {block_name: selected_option}
|
|
model_profile: "sdxl" or "flux"
|
|
|
|
Returns:
|
|
Final composed prompt string
|
|
"""
|
|
library = self.libraries.get(library_id, {})
|
|
blocks = library.get("blocks", {})
|
|
|
|
structure = self.schema["structure"]
|
|
weights = self.schema["weights"]
|
|
|
|
parts = []
|
|
block_order = structure["block_order"]
|
|
separator = structure["separator"]
|
|
break_positions = structure.get("break_positions", [])
|
|
|
|
for i, block_name in enumerate(block_order):
|
|
selected_option = selections.get(block_name)
|
|
if not selected_option:
|
|
continue
|
|
|
|
block_data = blocks.get(block_name, {})
|
|
text = block_data.get(selected_option, "")
|
|
|
|
if not text:
|
|
continue
|
|
|
|
weight = weights.get(block_name, 1.0)
|
|
weighted_text = self.apply_weight(text, weight, model_profile)
|
|
|
|
parts.append(weighted_text)
|
|
|
|
if (i + 1) in break_positions:
|
|
parts.append("BREAK")
|
|
|
|
return separator.join(parts)
|