384 lines
14 KiB
Python
384 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
try:
|
|
from .character_slot import parse_character_cast
|
|
from .hardcore_position_config import (
|
|
build_hardcore_position_pool_json,
|
|
hardcore_position_family_choices,
|
|
hardcore_position_key_choices,
|
|
normalize_hardcore_position_family,
|
|
normalize_hardcore_position_values,
|
|
)
|
|
from .location_config import build_composition_pool_json, build_location_pool_json
|
|
except ImportError: # Allows local smoke tests from the repository root.
|
|
from character_slot import parse_character_cast
|
|
from hardcore_position_config import (
|
|
build_hardcore_position_pool_json,
|
|
hardcore_position_family_choices,
|
|
hardcore_position_key_choices,
|
|
normalize_hardcore_position_family,
|
|
normalize_hardcore_position_values,
|
|
)
|
|
from location_config import build_composition_pool_json, build_location_pool_json
|
|
|
|
|
|
SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG"
|
|
SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG"
|
|
SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
|
|
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
|
|
|
|
LOCK_CHOICES = [
|
|
"none",
|
|
"location_from_softcore",
|
|
"location_from_hardcore",
|
|
"composition_from_softcore",
|
|
"composition_from_hardcore",
|
|
"hardcore_position_current",
|
|
"softcore_outfit_current",
|
|
]
|
|
WARDROBE_SUBJECTS = ["Woman A", "Man A", "all women", "all men", "all"]
|
|
|
|
|
|
def _loads(value: Any) -> dict[str, Any]:
|
|
if not value:
|
|
return {}
|
|
if isinstance(value, dict):
|
|
return value
|
|
try:
|
|
loaded = json.loads(str(value))
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
return loaded if isinstance(loaded, dict) else {}
|
|
|
|
|
|
def _dump(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
|
|
|
|
def _text(value: Any) -> str:
|
|
return str(value or "").strip()
|
|
|
|
|
|
def _row(meta: dict[str, Any], key: str) -> dict[str, Any]:
|
|
value = meta.get(key)
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def _first_text(*values: Any) -> str:
|
|
for value in values:
|
|
text = _text(value)
|
|
if text:
|
|
return text
|
|
return ""
|
|
|
|
|
|
def _first_position_key(row: dict[str, Any], meta: dict[str, Any]) -> str:
|
|
values: list[Any] = []
|
|
if row.get("position_key") is not None:
|
|
values.append(row.get("position_key"))
|
|
if row.get("position_keys") is not None:
|
|
position_keys = row.get("position_keys")
|
|
values.extend(position_keys if isinstance(position_keys, list) else [position_keys])
|
|
config = meta.get("hardcore_position_config")
|
|
if isinstance(config, dict) and config.get("positions") is not None:
|
|
positions = config.get("positions")
|
|
values.extend(positions if isinstance(positions, list) else [positions])
|
|
selected = normalize_hardcore_position_values(values)
|
|
return selected[0] if selected else ""
|
|
|
|
|
|
def _position_family(row: dict[str, Any], meta: dict[str, Any]) -> str:
|
|
config = meta.get("hardcore_position_config")
|
|
raw_config_family = config.get("family") if isinstance(config, dict) else ""
|
|
return normalize_hardcore_position_family(
|
|
_first_text(row.get("position_family"), row.get("source_position_family"), raw_config_family),
|
|
"any",
|
|
)
|
|
|
|
|
|
def _choice_rows(meta: dict[str, Any]) -> list[dict[str, Any]]:
|
|
soft = _row(meta, "softcore_row")
|
|
hard = _row(meta, "hardcore_row")
|
|
options = meta.get("options") if isinstance(meta.get("options"), dict) else {}
|
|
rows = [
|
|
("softcore.level", "softcore", "Softcore Level", options.get("softcore_level")),
|
|
("softcore.cast", "softcore", "Softcore Cast", options.get("softcore_cast")),
|
|
("softcore.outfit", "softcore", "Softcore Outfit", soft.get("item")),
|
|
("softcore.pose", "softcore", "Softcore Pose", soft.get("pose")),
|
|
("softcore.expression", "softcore", "Softcore Expression", soft.get("expression")),
|
|
("hardcore.level", "hardcore", "Hardcore Level", options.get("hardcore_level")),
|
|
("hardcore.cast", "hardcore", "Hardcore Cast", options.get("hardcore_cast")),
|
|
("hardcore.position_family", "hardcore", "Position Family", _position_family(hard, meta)),
|
|
("hardcore.position_key", "hardcore", "Position Key", _first_position_key(hard, meta)),
|
|
("hardcore.role_graph", "hardcore", "Role Graph", hard.get("role_graph")),
|
|
("hardcore.action", "hardcore", "Action Text", hard.get("item")),
|
|
("hardcore.expression", "hardcore", "Hardcore Expression", hard.get("expression")),
|
|
("scene.softcore", "scene", "Softcore Location", soft.get("scene_text")),
|
|
("scene.hardcore", "scene", "Hardcore Location", hard.get("scene_text")),
|
|
("composition.softcore", "composition", "Softcore Composition", soft.get("composition")),
|
|
("composition.hardcore", "composition", "Hardcore Composition", hard.get("composition")),
|
|
("camera.softcore", "camera", "Softcore Camera", meta.get("softcore_camera_directive")),
|
|
("camera.hardcore", "camera", "Hardcore Camera", meta.get("hardcore_camera_directive")),
|
|
("style.softcore", "style", "Softcore Style", soft.get("positive_suffix")),
|
|
("style.hardcore", "style", "Hardcore Style", hard.get("positive_suffix")),
|
|
]
|
|
return [
|
|
{
|
|
"axis": axis,
|
|
"branch": branch,
|
|
"label": label,
|
|
"value": _text(value),
|
|
"active": bool(_text(value)),
|
|
}
|
|
for axis, branch, label, value in rows
|
|
if _text(value)
|
|
]
|
|
|
|
|
|
def _metadata_slots(meta: dict[str, Any]) -> list[dict[str, Any]]:
|
|
slots = meta.get("character_cast_slots")
|
|
if isinstance(slots, list):
|
|
return parse_character_cast(slots)
|
|
chain = meta.get("scene_chain") if isinstance(meta.get("scene_chain"), dict) else {}
|
|
for branch in ("hardcore", "softcore"):
|
|
branch_scene = chain.get(branch) if isinstance(chain.get(branch), dict) else {}
|
|
configs = branch_scene.get("configs") if isinstance(branch_scene.get("configs"), dict) else {}
|
|
parsed = parse_character_cast(configs.get("character_cast"))
|
|
if parsed:
|
|
return parsed
|
|
return []
|
|
|
|
|
|
def _slot_matches_subject(slot: dict[str, Any], subject: str) -> bool:
|
|
subject = _text(subject).lower()
|
|
subject_type = _text(slot.get("subject_type")).lower()
|
|
label = _text(slot.get("label")).upper()
|
|
if subject == "all":
|
|
return True
|
|
if subject == "all women":
|
|
return subject_type == "woman"
|
|
if subject == "all men":
|
|
return subject_type == "man"
|
|
if subject.startswith("woman "):
|
|
return subject_type == "woman" and label == subject.split(" ", 1)[1].upper()
|
|
if subject.startswith("man "):
|
|
return subject_type == "man" and label == subject.split(" ", 1)[1].upper()
|
|
return False
|
|
|
|
|
|
def _wardrobe_character_cast(
|
|
meta: dict[str, Any],
|
|
subject: str,
|
|
softcore_outfit: str,
|
|
hardcore_clothing: str,
|
|
) -> tuple[str, int]:
|
|
slots = _metadata_slots(meta)
|
|
if not slots:
|
|
return "", 0
|
|
changed = 0
|
|
for slot in slots:
|
|
if not _slot_matches_subject(slot, subject):
|
|
continue
|
|
if softcore_outfit:
|
|
slot["softcore_outfit"] = softcore_outfit
|
|
changed += 1
|
|
if hardcore_clothing:
|
|
slot["hardcore_clothing"] = hardcore_clothing
|
|
changed += 1
|
|
if not changed:
|
|
return _dump({"profile_type": "character_cast", "version": 1, "slots": slots}), 0
|
|
return _dump({"profile_type": "character_cast", "version": 1, "slots": slots}), changed
|
|
|
|
|
|
def _lock_location(meta: dict[str, Any], lock_choice: str) -> str:
|
|
soft = _row(meta, "softcore_row")
|
|
hard = _row(meta, "hardcore_row")
|
|
if lock_choice == "location_from_softcore":
|
|
return _text(soft.get("scene_text"))
|
|
if lock_choice == "location_from_hardcore":
|
|
return _text(hard.get("scene_text"))
|
|
return ""
|
|
|
|
|
|
def _lock_composition(meta: dict[str, Any], lock_choice: str) -> str:
|
|
soft = _row(meta, "softcore_row")
|
|
hard = _row(meta, "hardcore_row")
|
|
if lock_choice == "composition_from_softcore":
|
|
return _text(soft.get("composition"))
|
|
if lock_choice == "composition_from_hardcore":
|
|
return _text(hard.get("composition"))
|
|
return ""
|
|
|
|
|
|
def _summary_parts(*parts: str) -> str:
|
|
clean = [part for part in (_text(part) for part in parts) if part]
|
|
return "; ".join(clean) if clean else "no overrides active"
|
|
|
|
|
|
class SxCPChoiceBoard:
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"metadata_json": ("STRING", {"default": "", "multiline": True}),
|
|
"lock_choice": (LOCK_CHOICES, {"default": "none"}),
|
|
"location_override": ("STRING", {"default": "", "multiline": True}),
|
|
"composition_override": ("STRING", {"default": "", "multiline": True}),
|
|
"hardcore_position_family": (["auto"] + hardcore_position_family_choices(), {"default": "auto"}),
|
|
"hardcore_position_key": (["auto"] + hardcore_position_key_choices(), {"default": "auto"}),
|
|
"wardrobe_subject": (WARDROBE_SUBJECTS, {"default": "Woman A"}),
|
|
"softcore_outfit_override": ("STRING", {"default": "", "multiline": True}),
|
|
"hardcore_clothing_override": ("STRING", {"default": "", "multiline": True}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
SXCP_LOCATION_CONFIG,
|
|
SXCP_COMPOSITION_CONFIG,
|
|
SXCP_HARDCORE_POSITION_CONFIG,
|
|
SXCP_CHARACTER_CAST,
|
|
"STRING",
|
|
"STRING",
|
|
)
|
|
RETURN_NAMES = (
|
|
"location_config",
|
|
"composition_config",
|
|
"hardcore_position_config",
|
|
"character_cast",
|
|
"choice_board_json",
|
|
"summary",
|
|
)
|
|
FUNCTION = "build"
|
|
CATEGORY = "prompt_builder/v2_scene"
|
|
|
|
def build(
|
|
self,
|
|
metadata_json,
|
|
lock_choice,
|
|
location_override,
|
|
composition_override,
|
|
hardcore_position_family,
|
|
hardcore_position_key,
|
|
wardrobe_subject,
|
|
softcore_outfit_override,
|
|
hardcore_clothing_override,
|
|
):
|
|
meta = _loads(metadata_json)
|
|
soft = _row(meta, "softcore_row")
|
|
hard = _row(meta, "hardcore_row")
|
|
|
|
location_text = _text(location_override) or _lock_location(meta, lock_choice)
|
|
composition_text = _text(composition_override) or _lock_composition(meta, lock_choice)
|
|
|
|
family = _text(hardcore_position_family)
|
|
key = _text(hardcore_position_key)
|
|
if lock_choice == "hardcore_position_current":
|
|
if family == "auto":
|
|
family = _position_family(hard, meta)
|
|
if key == "auto":
|
|
key = _first_position_key(hard, meta)
|
|
if family == "auto":
|
|
family = "any"
|
|
if key == "auto":
|
|
key = ""
|
|
|
|
softcore_outfit = _text(softcore_outfit_override)
|
|
if lock_choice == "softcore_outfit_current" and not softcore_outfit:
|
|
softcore_outfit = _text(soft.get("item"))
|
|
hardcore_clothing = _text(hardcore_clothing_override)
|
|
|
|
location_config = (
|
|
build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations=location_text,
|
|
)
|
|
if location_text
|
|
else ""
|
|
)
|
|
composition_config = (
|
|
build_composition_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_compositions=composition_text,
|
|
)
|
|
if composition_text
|
|
else ""
|
|
)
|
|
selected_positions = [key] if key else []
|
|
position_active = bool(selected_positions) or family != "any"
|
|
hardcore_position_config = (
|
|
build_hardcore_position_pool_json(
|
|
combine_mode="replace",
|
|
family=family,
|
|
selected_positions=selected_positions,
|
|
)
|
|
if position_active
|
|
else ""
|
|
)
|
|
character_cast, wardrobe_changes = _wardrobe_character_cast(
|
|
meta,
|
|
wardrobe_subject,
|
|
softcore_outfit,
|
|
hardcore_clothing,
|
|
) if softcore_outfit or hardcore_clothing else ("", 0)
|
|
|
|
board = {
|
|
"version": 1,
|
|
"choices": _choice_rows(meta),
|
|
"overrides": {
|
|
"lock_choice": lock_choice,
|
|
"location": location_text,
|
|
"composition": composition_text,
|
|
"hardcore_position_family": family if position_active else "",
|
|
"hardcore_position_key": key,
|
|
"wardrobe_subject": wardrobe_subject,
|
|
"softcore_outfit": softcore_outfit,
|
|
"hardcore_clothing": hardcore_clothing,
|
|
},
|
|
"outputs": {
|
|
"location_config": bool(location_config),
|
|
"composition_config": bool(composition_config),
|
|
"hardcore_position_config": bool(hardcore_position_config),
|
|
"character_cast": bool(character_cast),
|
|
"wardrobe_changes": wardrobe_changes,
|
|
},
|
|
}
|
|
summary = _summary_parts(
|
|
f"location locked" if location_config else "",
|
|
f"composition locked" if composition_config else "",
|
|
f"position locked ({family}{':' + key if key else ''})" if hardcore_position_config else "",
|
|
f"wardrobe updated x{wardrobe_changes}" if wardrobe_changes else "",
|
|
)
|
|
board_json = _dump(board)
|
|
return {
|
|
"ui": {
|
|
"choice_board_json": [board_json],
|
|
"summary": [summary],
|
|
},
|
|
"result": (
|
|
location_config,
|
|
composition_config,
|
|
hardcore_position_config,
|
|
character_cast,
|
|
board_json,
|
|
summary,
|
|
),
|
|
}
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"SxCPChoiceBoard": SxCPChoiceBoard,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"SxCPChoiceBoard": "SxCP Choice Board",
|
|
}
|