Implement structured H3 reference objects

This commit is contained in:
2026-08-26 16:19:35 +00:00
parent 0b20381c5c
commit c37498c175
6 changed files with 1042 additions and 214 deletions
+265 -53
View File
@@ -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",
}