From 98833ba99d3c63c0d4d650f63c237a22d79825c2 Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Fri, 4 Sep 2026 15:52:40 +0000 Subject: [PATCH] Restore general character and location helpers --- README.md | 12 +- dumas_image_nodes.py | 337 +++++++++++++++++++++++++++++++- tests/test_dumas_image_nodes.py | 65 ++++++ 3 files changed, 409 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cdb473d..fa6be5f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,16 @@ - Output: `reference` - Builds one structured `REFERENCE` object for a location/environment so H3 can use the same socket type for both character and scenic refs. +- `Dumas Character Helper` + - Inputs: `image1`, `image2`, picture IDs, character identity fields, `general`, `wardrobe` + - Outputs: `image1`, `image2`, `reference_prompt`, `wardrobe` + - Restores the original general-purpose helper shape: pass two images through unchanged and emit prompt text/wardrobe text for manual wiring. + +- `Dumas Location Helper` + - Inputs: `image1`, `image2`, picture IDs, `location_id`, `name`, `alias`, `description`, `general` + - Outputs: `image1`, `image2`, `reference_prompt` + - Matching general-purpose helper for environments/locations: pass two images through unchanged and emit location reference prompt text. + - `Dumas Anchor Style` - Inputs: `anchor_style`, `style_description` - Output: `anchor` @@ -239,7 +249,7 @@ decr -> use index - 1 `Dumas H3 Plan Attach Scene Images` and `Dumas H3 Plan Extract Scene Images` are a companion pair for `ComfyUI-MiniMaxH3-Contex-Loop` and the local `ref2v` lane. The upstream H3 plan node cannot dynamically grow nine new image sockets for every JSON-defined scene, so Dumas stores scene image bindings beside the plan using a lightweight token and an in-memory registry. That keeps `plan.json` archiving intact while still letting you wire up nine IMAGE sockets per scene through chained helper nodes. -`Dumas Character Reference` and `Dumas Location Reference` live in `Dumas/MiniMax`. Both output a structured `REFERENCE` object that carries the image plus its semantic payload. `Dumas H3 Long Videos` accepts those `REFERENCE` sockets directly on `ref_1`..`ref_9`, resolves `` against the wired slot positions, and can also pull character wardrobe context from the structured ref data when `character_memory` is left blank. +`Dumas Character Helper` is the restored two-image/text helper for general H3 workflows, and `Dumas Location Helper` mirrors it for scene/environment references. The structured `Dumas Character Reference` and `Dumas Location Reference` nodes remain available separately for workflows that still want a single `REFERENCE` socket. `Dumas Strip Iteration Suffix` keeps the part before the first underscore and drops the rest. Names like `char123_pose_final.png` become `char123.png`, while names with no underscore such as `char123.png` are left untouched. diff --git a/dumas_image_nodes.py b/dumas_image_nodes.py index c507ee6..7cb87f0 100644 --- a/dumas_image_nodes.py +++ b/dumas_image_nodes.py @@ -1042,6 +1042,65 @@ def _build_character_wardrobe_text(wardrobe, character_id, name, alias): return text +def _label_for_location(name, location_id): + return _normalize_free_text(name) or _normalize_free_text(location_id) or "the location" + + +def _build_location_helper_text( + primary_picture_id, + secondary_picture_id, + location_id, + name, + alias, + description, + general, +): + primary_picture = int(primary_picture_id) + secondary_picture = int(secondary_picture_id) + location_name = _normalize_free_text(name) + location_id = _normalize_free_text(location_id) + alias = _normalize_free_text(alias) + description = _ensure_sentence(description) + general = _ensure_sentence(general) + location_label = _label_for_location(location_name, location_id) + + if location_name: + first_line = ( + f" and reference " + f"the same location called {location_name}." + ) + elif location_id: + first_line = ( + f" and reference " + f'the same location with ID "{location_id}".' + ) + else: + first_line = ( + f" and reference " + "the same location." + ) + + lines = [ + first_line, + f" is the primary wide/environment reference for {location_label}.", + f" is the secondary detail/angle reference for {location_label}.", + ] + + facts = [] + if alias: + facts.append(f"is also known as {alias}") + if description: + facts.append(description) + + if facts: + lines.append(f"{location_label} {', '.join(facts)}") + + if general: + lines.append(general) + + return "\n".join(lines) + + class DumasImageCompareNode: DESCRIPTION = ( "Dumas Image Compare shows the difference between two images directly on " @@ -1604,6 +1663,274 @@ class DumasH3PlanExtractSceneImagesNode: return (passthrough_plan, *images, _connected_image_count(images)) +class DumasCharacterHelperNode: + DESCRIPTION = ( + "Build a general character reference prompt and wardrobe sheet from two " + "IMAGE sockets plus simple identity fields, while passing both images " + "through unchanged." + ) + RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "STRING") + RETURN_NAMES = ("image1", "image2", "reference_prompt", "wardrobe") + FUNCTION = "build_character_text" + 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": ( + ["1", "2", "3", "4", "5", "6", "7", "8", "9"], + { + "default": "1", + "tooltip": "Picture number to mention for image1 in the 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 reference prompt.", + }, + ), + "character_id": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional character ID string to include in the output text.", + }, + ), + "name": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Character name used in the main reference sentences.", + }, + ), + "alias": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional alternate name, codename, or nickname.", + }, + ), + "gender": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional gender field for non-visual character facts.", + }, + ), + "age": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional numeric age. Invalid values are omitted.", + }, + ), + "nationality": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional nationality, origin, or cultural background.", + }, + ), + "occupation": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional job, role, or function that is not visually obvious.", + }, + ), + "height_feet": ( + ["", "3", "4", "5", "6", "7", "8"], + { + "default": "", + "tooltip": "Optional feet component for the character's height.", + }, + ), + "height_inches": ( + ["", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], + { + "default": "", + "tooltip": "Optional inches component for the character's height.", + }, + ), + "accent": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional short accent description.", + }, + ), + "general": ( + "STRING", + { + "default": "", + "multiline": True, + "tooltip": "Optional non-clothing details appended as the last sentence of the reference prompt.", + }, + ), + "wardrobe": ( + "STRING", + { + "default": "", + "multiline": True, + "tooltip": "Optional wardrobe/channel text. Plain clothing lists are auto-wrapped as 'Name = ...' when a name, alias, or character ID is present.", + }, + ), + } + } + + def build_character_text( + self, + image1, + image2, + image1_picture_id, + image2_picture_id, + character_id, + name, + alias, + gender, + age, + nationality, + occupation, + height_feet, + height_inches, + accent, + 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, + ) + wardrobe_text = _build_character_wardrobe_text( + wardrobe, + character_id, + name, + alias, + ) + return (image1, image2, text, wardrobe_text) + + +class DumasLocationHelperNode: + DESCRIPTION = ( + "Build a general location reference prompt from two IMAGE sockets plus " + "simple environment fields, while passing both images through unchanged." + ) + RETURN_TYPES = ("IMAGE", "IMAGE", "STRING") + RETURN_NAMES = ("image1", "image2", "reference_prompt") + FUNCTION = "build_location_text" + CATEGORY = "Dumas/MiniMax" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image1": ("IMAGE", {"tooltip": "Primary location image to pass through and describe."}), + "image2": ("IMAGE", {"tooltip": "Secondary location image to pass through and describe."}), + "image1_picture_id": ( + ["1", "2", "3", "4", "5", "6", "7", "8", "9"], + { + "default": "1", + "tooltip": "Picture number to mention for image1 in the 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 reference prompt.", + }, + ), + "location_id": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional location ID string to include in the output text.", + }, + ), + "name": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Location name used in the main reference sentences.", + }, + ), + "alias": ( + "STRING", + { + "default": "", + "multiline": False, + "tooltip": "Optional alternate name, label, or area name.", + }, + ), + "description": ( + "STRING", + { + "default": "", + "multiline": True, + "tooltip": "Persistent environment, layout, and atmosphere description.", + }, + ), + "general": ( + "STRING", + { + "default": "", + "multiline": True, + "tooltip": "Optional extra notes appended as the last sentence of the reference prompt.", + }, + ), + } + } + + def build_location_text( + self, + image1, + image2, + image1_picture_id, + image2_picture_id, + location_id, + name, + alias, + description, + general, + ): + text = _build_location_helper_text( + image1_picture_id, + image2_picture_id, + location_id, + name, + alias, + description, + general, + ) + return (image1, image2, text) + + class DumasCharacterReferenceNode: DESCRIPTION = ( "Build one structured REFERENCE object for a character so H3 can carry " @@ -1888,8 +2215,9 @@ NODE_CLASS_MAPPINGS = { "DumasCharacterReference": DumasCharacterReferenceNode, "DumasLocationReference": DumasLocationReferenceNode, "DumasAnchorStyle": DumasAnchorStyleNode, - "DumasCharacterHelper": DumasCharacterReferenceNode, - "DumasH3CharacterHelper": DumasCharacterReferenceNode, + "DumasCharacterHelper": DumasCharacterHelperNode, + "DumasLocationHelper": DumasLocationHelperNode, + "DumasH3CharacterHelper": DumasCharacterHelperNode, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -1901,6 +2229,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "DumasCharacterReference": "Dumas Character Reference", "DumasLocationReference": "Dumas Location Reference", "DumasAnchorStyle": "Dumas Anchor Style", - "DumasCharacterHelper": "Dumas Character Reference", - "DumasH3CharacterHelper": "Dumas Character Reference", + "DumasCharacterHelper": "Dumas Character Helper", + "DumasLocationHelper": "Dumas Location Helper", + "DumasH3CharacterHelper": "Dumas Character Helper", } diff --git a/tests/test_dumas_image_nodes.py b/tests/test_dumas_image_nodes.py index cd73de6..c10b9fe 100644 --- a/tests/test_dumas_image_nodes.py +++ b/tests/test_dumas_image_nodes.py @@ -356,6 +356,71 @@ class DumasImageNodeTests(unittest.TestCase): required = self.image_nodes.DumasLocationReferenceNode.INPUT_TYPES()["required"] self.assertNotIn("picture_id", required) + def test_character_helper_restores_image_and_text_outputs(self): + node = self.image_nodes.DumasCharacterHelperNode() + image1 = FakeTensorBatch() + image2 = FakeTensorBatch() + + result = node.build_character_text( + image1=image1, + image2=image2, + image1_picture_id="1", + image2_picture_id="2", + character_id="char_dave", + name="Dave", + alias="The Locksmith", + gender="male", + age="41", + nationality="English", + occupation="a detective", + height_feet="6", + height_inches="2", + accent="English", + general="Moves carefully and notices every exit", + wardrobe="weathered red flight jacket, grey cargo shorts, black boots", + ) + + self.assertIs(result[0], image1) + self.assertIs(result[1], image2) + self.assertIn(" and reference the same character", result[2]) + self.assertIn("Dave is also known as The Locksmith", result[2]) + self.assertIn("is 41 years old", result[2]) + self.assertEqual(result[3], "Dave = weathered red flight jacket, grey cargo shorts, black boots") + + def test_location_helper_matches_character_helper_shape_without_wardrobe(self): + node = self.image_nodes.DumasLocationHelperNode() + image1 = FakeTensorBatch() + image2 = FakeTensorBatch() + + result = node.build_location_text( + image1=image1, + image2=image2, + image1_picture_id="3", + image2_picture_id="4", + location_id="coffee-shop-01", + name="Coffee Shop", + alias="Cafe Interior", + description="Warm tungsten lighting, narrow counter, rainy front window", + general="Evening ambience, cramped but cozy", + ) + + self.assertIs(result[0], image1) + self.assertIs(result[1], image2) + self.assertIn(" and reference the same location", result[2]) + self.assertIn("Coffee Shop is also known as Cafe Interior", result[2]) + self.assertIn("Warm tungsten lighting, narrow counter, rainy front window.", result[2]) + self.assertIn("Evening ambience, cramped but cozy.", result[2]) + self.assertEqual(len(result), 3) + + def test_helper_node_mappings_use_general_purpose_helpers(self): + mappings = self.image_nodes.NODE_CLASS_MAPPINGS + display = self.image_nodes.NODE_DISPLAY_NAME_MAPPINGS + + self.assertIs(mappings["DumasCharacterHelper"], self.image_nodes.DumasCharacterHelperNode) + self.assertIs(mappings["DumasLocationHelper"], self.image_nodes.DumasLocationHelperNode) + self.assertEqual(display["DumasCharacterHelper"], "Dumas Character Helper") + self.assertEqual(display["DumasLocationHelper"], "Dumas Location Helper") + def test_normalize_reference_upgrades_generic_summary_with_socket_picture_id(self): image = FakeTensorBatch()