Files
ComfyUI-Ethanfel-Prompt-Bui…/row_rendering.py

150 lines
5.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from string import Formatter
from typing import Any
try:
from . import category_library as category_policy
from . import generate_prompt_batches as g
from . import row_camera as row_camera_policy
from . import style_config as style_config_policy
except ImportError: # Allows local smoke tests from the repository root.
import category_library as category_policy
import generate_prompt_batches as g
import row_camera as row_camera_policy
import style_config as style_config_policy
GENERIC_POSITIVE_SUFFIX = (
"Use coherent anatomy, readable body placement, natural light response, "
"clear material texture, stable spatial depth, and polished visual detail."
)
DEFAULT_STYLE = "realistic adult scene with natural camera realism"
@dataclass(frozen=True)
class RowTextFields:
negative_prompt: str
positive_suffix: str
style: str
item_label: str
SINGLE_TEMPLATE = (
"A {subject}: {style}, {age}, {body_phrase}, {skin}, {hair}, {eyes}. "
"{item_label}: {item}. Scene: {scene}. Pose: {pose}. Facial expression: {expression}. "
"Composition: {composition_prompt}. {positive_suffix} Avoid: {negative_prompt}."
)
COUPLE_TEMPLATE = (
"{subject_phrase}: {style}. Ages: {age}. Body types: {body}. {item_label}: {item}. "
"Scene: {scene}. Pose: {pose}. Facial expressions: {expression}. "
"Composition: {composition_prompt}. {positive_suffix} Avoid: {negative_prompt}."
)
GROUP_TEMPLATE = (
"{subject_phrase}: {style}, ages {age}, diverse adult body types. {item_label}: {item}. "
"Scene: {scene}. Facial expressions: {expression}. Composition: {composition_prompt}. "
"{positive_suffix} Avoid: {negative_prompt}."
)
LAYOUT_TEMPLATE = (
"{item}: {style}, adults only, clean designed composition. Scene: {scene}. "
"Facial expression: {expression}. Composition: {composition}. {positive_suffix} "
"Avoid: {negative_prompt}. Use no readable text unless the layout naturally needs small decorative placeholder marks."
)
DEFAULT_CAPTION_TEMPLATE = (
"{trigger}, {subject_phrase}, {age}, {item}, {scene}, {composition}"
)
class SafeFormatDict(dict):
def __missing__(self, key: str) -> str:
return "{" + key + "}"
def format_template(template: str, context: dict[str, Any]) -> str:
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
safe_context = SafeFormatDict({key: str(value) for key, value in context.items()})
for field in fields:
safe_context.setdefault(field, "{" + field + "}")
return template.format_map(safe_context)
def resolve_row_text_fields(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
style_config: str | dict[str, Any] | None = None,
) -> RowTextFields:
base_negative = str(category_policy.merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
base_suffix = str(category_policy.merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
base_style = str(category_policy.merged_field(category, subcategory, item, "style", DEFAULT_STYLE))
style, positive_suffix = style_config_policy.resolve_style_fields(base_style, base_suffix, style_config)
return RowTextFields(
negative_prompt=style_config_policy.merge_negative_prompt(base_negative, style_config),
positive_suffix=positive_suffix,
style=style,
item_label=str(category_policy.merged_field(category, subcategory, item, "item_label", category["name"])),
)
def default_prompt_template(subject_type: str) -> str:
if subject_type in ("woman", "man"):
return SINGLE_TEMPLATE
if subject_type == "couple":
return COUPLE_TEMPLATE
if subject_type == "group":
return GROUP_TEMPLATE
return LAYOUT_TEMPLATE
def prompt_template_for(item: Any, subcategory: dict[str, Any], category: dict[str, Any], subject_type: str) -> str:
if isinstance(item, dict) and "prompt_template" in item:
return str(item["prompt_template"])
template = str(subcategory.get("prompt_template") or category.get("prompt_template") or "")
return template or default_prompt_template(subject_type)
def caption_template_for(item: Any, subcategory: dict[str, Any], category: dict[str, Any]) -> str:
return str(
(item.get("caption_template") if isinstance(item, dict) else None)
or subcategory.get("caption_template")
or category.get("caption_template")
or DEFAULT_CAPTION_TEMPLATE
)
def render_prompt_caption(
*,
item: Any,
subcategory: dict[str, Any],
category: dict[str, Any],
subject_type: str,
context: dict[str, Any],
cast_descriptor_text: str = "",
pov_prompt_directive: str = "",
) -> dict[str, str]:
prompt_template = prompt_template_for(item, subcategory, category, subject_type)
caption_template = caption_template_for(item, subcategory, category)
prompt = format_template(prompt_template, context)
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in prompt_template:
prompt = row_camera_policy.insert_positive_directive(prompt, f"Characters: {cast_descriptor_text}.")
if subject_type == "configured_cast" and pov_prompt_directive:
prompt = row_camera_policy.insert_positive_directive(prompt, pov_prompt_directive)
caption = format_template(caption_template, context)
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in caption_template:
caption = f"{caption.rstrip()}, {cast_descriptor_text}"
return {
"prompt": prompt,
"caption": caption,
"prompt_template": prompt_template,
"caption_template": caption_template,
}