Add modular prompt system with clean architecture
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.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user