Implement structured H3 reference objects
This commit is contained in:
+265
-53
@@ -20,6 +20,7 @@ _DATE_TOKEN_RE = re.compile(r"%date:([^%]+)%")
|
||||
_SERVE_TOKENS = OrderedDict()
|
||||
_SERVE_CAP = 256
|
||||
_H3_PLAN_TYPE = "H3_CHAIN_PLAN"
|
||||
_REFERENCE_TYPE = "REFERENCE"
|
||||
_H3_PLAN_IMAGE_BINDINGS_KEY = "_dumas_scene_image_bindings"
|
||||
_H3_PLAN_IMAGE_BINDINGS = OrderedDict()
|
||||
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
||||
@@ -454,6 +455,135 @@ def _ensure_sentence(value):
|
||||
return text
|
||||
|
||||
|
||||
def _slug_like(value):
|
||||
text = _normalize_free_text(value).lower()
|
||||
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
|
||||
return text
|
||||
|
||||
|
||||
def _parse_aliases(value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
raw_items = value
|
||||
else:
|
||||
raw_items = re.split(r"[,;\n\r]+", str(value or ""))
|
||||
aliases = []
|
||||
seen = set()
|
||||
for item in raw_items:
|
||||
alias = _normalize_free_text(item)
|
||||
if not alias:
|
||||
continue
|
||||
key = alias.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
aliases.append(alias)
|
||||
return aliases
|
||||
|
||||
|
||||
def _coerce_picture_id(value):
|
||||
try:
|
||||
number = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return number if number > 0 else None
|
||||
|
||||
|
||||
def _reference_id(explicit_id, name, fallback_prefix):
|
||||
explicit = _slug_like(explicit_id)
|
||||
if explicit:
|
||||
return explicit
|
||||
derived = _slug_like(name)
|
||||
if derived:
|
||||
return derived
|
||||
return f"{fallback_prefix}-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
|
||||
def _reference_label(picture_id):
|
||||
return f"<Picture {picture_id}>" if picture_id else ""
|
||||
|
||||
|
||||
def _reference_summary(kind, name, picture_id):
|
||||
label = _reference_label(picture_id)
|
||||
subject = _normalize_free_text(name) or ("character" if kind == "character" else "location")
|
||||
if label:
|
||||
return f"{subject} shown in {label}."
|
||||
return f"{subject} reference."
|
||||
|
||||
|
||||
def make_reference(
|
||||
*,
|
||||
kind,
|
||||
image,
|
||||
explicit_id="",
|
||||
name="",
|
||||
aliases="",
|
||||
picture_id=None,
|
||||
description="",
|
||||
wardrobe="",
|
||||
general="",
|
||||
facts=None,
|
||||
summary="",
|
||||
):
|
||||
normalized_name = _normalize_free_text(name)
|
||||
normalized_aliases = _parse_aliases(aliases)
|
||||
normalized_picture_id = _coerce_picture_id(picture_id)
|
||||
normalized_kind = "location" if str(kind or "").strip().lower() == "location" else "character"
|
||||
normalized_description = _normalize_free_text(description)
|
||||
normalized_wardrobe = _normalize_free_text(wardrobe)
|
||||
normalized_general = _normalize_free_text(general)
|
||||
normalized_facts = dict(facts or {})
|
||||
normalized_summary = _ensure_sentence(
|
||||
summary or _reference_summary(normalized_kind, normalized_name, normalized_picture_id)
|
||||
)
|
||||
return {
|
||||
"kind": normalized_kind,
|
||||
"id": _reference_id(explicit_id, normalized_name, normalized_kind),
|
||||
"name": normalized_name,
|
||||
"aliases": normalized_aliases,
|
||||
"picture_id": normalized_picture_id,
|
||||
"picture_label": _reference_label(normalized_picture_id),
|
||||
"image": image,
|
||||
"summary": normalized_summary,
|
||||
"description": normalized_description,
|
||||
"wardrobe": normalized_wardrobe if normalized_kind == "character" else "",
|
||||
"general": normalized_general,
|
||||
"facts": normalized_facts,
|
||||
}
|
||||
|
||||
|
||||
def normalize_reference(value, picture_id=None, allow_image_fallback=True):
|
||||
if isinstance(value, dict):
|
||||
reference = dict(value)
|
||||
image = reference.get("image")
|
||||
if image is None and allow_image_fallback:
|
||||
image = value
|
||||
reference["image"] = image
|
||||
if picture_id is not None and not reference.get("picture_id"):
|
||||
reference["picture_id"] = _coerce_picture_id(picture_id)
|
||||
reference["picture_label"] = _reference_label(reference.get("picture_id"))
|
||||
reference.setdefault("kind", "character")
|
||||
reference.setdefault("id", _reference_id("", reference.get("name"), reference["kind"]))
|
||||
reference.setdefault("name", "")
|
||||
reference["aliases"] = _parse_aliases(reference.get("aliases"))
|
||||
reference["summary"] = _ensure_sentence(
|
||||
reference.get("summary")
|
||||
or _reference_summary(reference["kind"], reference.get("name"), reference.get("picture_id"))
|
||||
)
|
||||
reference["description"] = _normalize_free_text(reference.get("description"))
|
||||
reference["wardrobe"] = _normalize_free_text(reference.get("wardrobe"))
|
||||
reference["general"] = _normalize_free_text(reference.get("general"))
|
||||
reference["facts"] = dict(reference.get("facts") or {})
|
||||
return reference
|
||||
if not allow_image_fallback:
|
||||
raise TypeError("Expected a REFERENCE object.")
|
||||
return make_reference(
|
||||
kind="character",
|
||||
image=value,
|
||||
picture_id=picture_id,
|
||||
summary="Plan-bound fallback reference.",
|
||||
)
|
||||
|
||||
|
||||
def _parse_positive_int(value):
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
@@ -1114,35 +1244,26 @@ class DumasH3PlanExtractSceneImagesNode:
|
||||
return (passthrough_plan, *images, _connected_image_count(images))
|
||||
|
||||
|
||||
class DumasCharacterHelperNode:
|
||||
class DumasCharacterReferenceNode:
|
||||
DESCRIPTION = (
|
||||
"Build a MiniMax H3-ready character reference prompt and wardrobe sheet "
|
||||
"from two IMAGE sockets plus simple identity fields, while passing both "
|
||||
"images through unchanged."
|
||||
"Build one structured REFERENCE object for a character so H3 can carry "
|
||||
"the image, identity description, wardrobe, and facts through one socket."
|
||||
)
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("image1", "image2", "reference_prompt", "wardrobe")
|
||||
FUNCTION = "build_character_text"
|
||||
RETURN_TYPES = (_REFERENCE_TYPE,)
|
||||
RETURN_NAMES = ("reference",)
|
||||
FUNCTION = "build_reference"
|
||||
CATEGORY = "Dumas/MiniMax"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image1": ("IMAGE", {"tooltip": "Primary image to pass through and describe."}),
|
||||
"image2": ("IMAGE", {"tooltip": "Secondary image to pass through and describe."}),
|
||||
"image1_picture_id": (
|
||||
"image": ("IMAGE", {"tooltip": "Character reference image."}),
|
||||
"picture_id": (
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
||||
{
|
||||
"default": "1",
|
||||
"tooltip": "Picture number to mention for image1 in the H3 reference prompt.",
|
||||
},
|
||||
),
|
||||
"image2_picture_id": (
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
||||
{
|
||||
"default": "2",
|
||||
"tooltip": "Picture number to mention for image2 in the H3 reference prompt.",
|
||||
"tooltip": "Authoring picture number for this reference.",
|
||||
},
|
||||
),
|
||||
"character_id": (
|
||||
@@ -1150,7 +1271,7 @@ class DumasCharacterHelperNode:
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Optional character ID string to include in the output text.",
|
||||
"tooltip": "Stable internal identifier for the character.",
|
||||
},
|
||||
),
|
||||
"name": (
|
||||
@@ -1158,7 +1279,7 @@ class DumasCharacterHelperNode:
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Character name used in the main reference sentences.",
|
||||
"tooltip": "Human-readable character name.",
|
||||
},
|
||||
),
|
||||
"alias": (
|
||||
@@ -1166,7 +1287,7 @@ class DumasCharacterHelperNode:
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Optional alternate name, codename, or nickname.",
|
||||
"tooltip": "Comma- or newline-separated aliases for name matching.",
|
||||
},
|
||||
),
|
||||
"gender": (
|
||||
@@ -1220,7 +1341,15 @@ class DumasCharacterHelperNode:
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Optional short accent description.",
|
||||
"tooltip": "Optional accent or speaking-style fact.",
|
||||
},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Persistent physical identity description for the character.",
|
||||
},
|
||||
),
|
||||
"general": (
|
||||
@@ -1228,7 +1357,7 @@ class DumasCharacterHelperNode:
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Optional non-clothing details appended as the last sentence of the H3 reference prompt.",
|
||||
"tooltip": "Optional freeform notes or extra context.",
|
||||
},
|
||||
),
|
||||
"wardrobe": (
|
||||
@@ -1236,18 +1365,16 @@ class DumasCharacterHelperNode:
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Optional H3 wardrobe/channel text. Plain clothing lists are auto-wrapped as 'Name = ...' when a name, alias, or character ID is present.",
|
||||
"tooltip": "Persistent clothing, styling, accessories, or look notes.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
def build_character_text(
|
||||
def build_reference(
|
||||
self,
|
||||
image1,
|
||||
image2,
|
||||
image1_picture_id,
|
||||
image2_picture_id,
|
||||
image,
|
||||
picture_id,
|
||||
character_id,
|
||||
name,
|
||||
alias,
|
||||
@@ -1258,31 +1385,112 @@ class DumasCharacterHelperNode:
|
||||
height_feet,
|
||||
height_inches,
|
||||
accent,
|
||||
description,
|
||||
general,
|
||||
wardrobe,
|
||||
):
|
||||
text = _build_character_helper_text(
|
||||
image1_picture_id,
|
||||
image2_picture_id,
|
||||
character_id,
|
||||
name,
|
||||
alias,
|
||||
gender,
|
||||
age,
|
||||
nationality,
|
||||
occupation,
|
||||
height_feet,
|
||||
height_inches,
|
||||
accent,
|
||||
general,
|
||||
reference = make_reference(
|
||||
kind="character",
|
||||
image=image,
|
||||
explicit_id=character_id,
|
||||
name=name,
|
||||
aliases=alias,
|
||||
picture_id=picture_id,
|
||||
description=description,
|
||||
wardrobe=wardrobe,
|
||||
general=general,
|
||||
facts={
|
||||
"gender": _normalize_free_text(gender),
|
||||
"age": str(_parse_positive_int(age) or ""),
|
||||
"nationality": _normalize_free_text(nationality),
|
||||
"occupation": _normalize_free_text(occupation),
|
||||
"height_feet": str(height_feet or "").strip(),
|
||||
"height_inches": str(height_inches or "").strip(),
|
||||
"accent": _normalize_free_text(accent),
|
||||
},
|
||||
)
|
||||
wardrobe_text = _build_character_wardrobe_text(
|
||||
wardrobe,
|
||||
character_id,
|
||||
name,
|
||||
alias,
|
||||
return (reference,)
|
||||
|
||||
|
||||
class DumasLocationReferenceNode:
|
||||
DESCRIPTION = (
|
||||
"Build one structured REFERENCE object for a location or environment so "
|
||||
"H3 can carry the image and environment description through one socket."
|
||||
)
|
||||
RETURN_TYPES = (_REFERENCE_TYPE,)
|
||||
RETURN_NAMES = ("reference",)
|
||||
FUNCTION = "build_reference"
|
||||
CATEGORY = "Dumas/MiniMax"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE", {"tooltip": "Location or environment reference image."}),
|
||||
"picture_id": (
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
||||
{
|
||||
"default": "1",
|
||||
"tooltip": "Authoring picture number for this reference.",
|
||||
},
|
||||
),
|
||||
"location_id": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Stable internal identifier for the location.",
|
||||
},
|
||||
),
|
||||
"name": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Human-readable location name.",
|
||||
},
|
||||
),
|
||||
"alias": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Comma- or newline-separated alternate location names.",
|
||||
},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Persistent environment, layout, and atmosphere description.",
|
||||
},
|
||||
),
|
||||
"general": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Optional freeform location notes.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
def build_reference(self, image, picture_id, location_id, name, alias, description, general):
|
||||
return (
|
||||
make_reference(
|
||||
kind="location",
|
||||
image=image,
|
||||
explicit_id=location_id,
|
||||
name=name,
|
||||
aliases=alias,
|
||||
picture_id=picture_id,
|
||||
description=description,
|
||||
general=general,
|
||||
facts={},
|
||||
),
|
||||
)
|
||||
return (image1, image2, text, wardrobe_text)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -1291,8 +1499,10 @@ NODE_CLASS_MAPPINGS = {
|
||||
"DumasLoadImagesFolder": DumasLoadImagesFolderNode,
|
||||
"DumasH3PlanAttachSceneImages": DumasH3PlanAttachSceneImagesNode,
|
||||
"DumasH3PlanExtractSceneImages": DumasH3PlanExtractSceneImagesNode,
|
||||
"DumasCharacterHelper": DumasCharacterHelperNode,
|
||||
"DumasH3CharacterHelper": DumasCharacterHelperNode,
|
||||
"DumasCharacterReference": DumasCharacterReferenceNode,
|
||||
"DumasLocationReference": DumasLocationReferenceNode,
|
||||
"DumasCharacterHelper": DumasCharacterReferenceNode,
|
||||
"DumasH3CharacterHelper": DumasCharacterReferenceNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -1301,6 +1511,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DumasLoadImagesFolder": "Load Images from Folder Dumas",
|
||||
"DumasH3PlanAttachSceneImages": "Dumas H3 Plan Attach Scene Images",
|
||||
"DumasH3PlanExtractSceneImages": "Dumas H3 Plan Extract Scene Images",
|
||||
"DumasCharacterHelper": "Dumas H3 Character Helper",
|
||||
"DumasH3CharacterHelper": "Dumas H3 Character Helper",
|
||||
"DumasCharacterReference": "Dumas Character Reference",
|
||||
"DumasLocationReference": "Dumas Location Reference",
|
||||
"DumasCharacterHelper": "Dumas Character Reference",
|
||||
"DumasH3CharacterHelper": "Dumas Character Reference",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user