diff --git a/README.md b/README.md index 610298d..948652f 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,9 @@ - `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. + - Outputs: `image1`, `image2`, `reference_prompt`, `wardrobe`, `reference1`, `reference2` + - Restores the original general-purpose helper shape while also emitting two structured `REFERENCE` objects for the prompt curator. + - The structured references carry the same character name, alias, age, height, gender, nationality, occupation, accent, wardrobe, and notes, so mentioning the character name in `Dumas H3 Prompt Curator` can include both helper images and the character facts automatically. - `Dumas Location Helper` - Inputs: `image1`, `image2`, picture IDs, `location_id`, `name`, `alias`, `description`, `general` diff --git a/dumas_image_nodes.py b/dumas_image_nodes.py index 7860513..8ca1069 100644 --- a/dumas_image_nodes.py +++ b/dumas_image_nodes.py @@ -1137,10 +1137,15 @@ def _reference_context(ref, compact_picture_number): def _subject_count_guard_text(selected_refs): character_labels = [] + seen_characters = set() for picture_number, (_slot, ref) in enumerate(selected_refs or (), 1): if ref.get("kind") != "character": continue name = _reference_text(ref.get("name")) or _reference_text(ref.get("id")) + key = (_reference_text(ref.get("id")) or name or f"picture-{picture_number}").lower() + if key in seen_characters: + continue + seen_characters.add(key) label = f"" if name: label = f"{label} {name}" @@ -1960,8 +1965,8 @@ class DumasCharacterHelperNode: "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") + RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "STRING", _REFERENCE_TYPE, _REFERENCE_TYPE) + RETURN_NAMES = ("image1", "image2", "reference_prompt", "wardrobe", "reference1", "reference2") FUNCTION = "build_character_text" CATEGORY = "Dumas/MiniMax" @@ -2122,7 +2127,36 @@ class DumasCharacterHelperNode: name, alias, ) - return (image1, image2, text, wardrobe_text) + 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), + } + common = { + "kind": "character", + "explicit_id": character_id, + "name": name, + "aliases": alias, + "description": general, + "wardrobe": wardrobe, + "general": general, + "facts": facts, + } + reference1 = make_reference( + image=image1, + summary="Primary full-body character reference.", + **common, + ) + reference2 = make_reference( + image=image2, + summary="Secondary facial character reference.", + **common, + ) + return (image1, image2, text, wardrobe_text, reference1, reference2) class DumasLocationHelperNode: diff --git a/tests/test_dumas_image_nodes.py b/tests/test_dumas_image_nodes.py index 6bc1308..511d9a0 100644 --- a/tests/test_dumas_image_nodes.py +++ b/tests/test_dumas_image_nodes.py @@ -386,6 +386,17 @@ class DumasImageNodeTests(unittest.TestCase): 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") + self.assertIs(result[4]["image"], image1) + self.assertIs(result[5]["image"], image2) + self.assertEqual(result[4]["id"], "char-dave") + self.assertEqual(result[5]["id"], "char-dave") + self.assertEqual(result[4]["name"], "Dave") + self.assertEqual(result[4]["aliases"], ["The Locksmith"]) + self.assertEqual(result[4]["facts"]["age"], "41") + self.assertEqual(result[4]["facts"]["height_feet"], "6") + self.assertEqual(result[4]["facts"]["height_inches"], "2") + self.assertEqual(result[4]["wardrobe"], "weathered red flight jacket, grey cargo shorts, black boots") + self.assertEqual(len(result), 6) def test_location_helper_matches_character_helper_shape_without_wardrobe(self): node = self.image_nodes.DumasLocationHelperNode() @@ -508,6 +519,47 @@ class DumasImageNodeTests(unittest.TestCase): self.assertIn("Only include the people explicitly described", result[0]) self.assertEqual(result[10], 0) + def test_h3_prompt_curator_treats_helper_image_pair_as_one_character(self): + helper = self.image_nodes.DumasCharacterHelperNode() + curator = self.image_nodes.DumasH3PromptCuratorNode() + image1 = FakeTensorBatch() + image2 = FakeTensorBatch() + helper_result = helper.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="detective", + height_feet="6", + height_inches="2", + accent="English", + general="Tired eyes, cropped brown hair", + wardrobe="weathered red flight jacket", + ) + + result = curator.curate_prompt( + action_prompt="Dave checks the locked door.", + anatomy_guard="on", + subject_count_guard="auto", + ref_1=helper_result[4], + ref_2=helper_result[5], + ) + + self.assertIs(result[1], image1) + self.assertIs(result[2], image2) + self.assertEqual(result[10], 2) + self.assertIn("Character facts for Dave", result[0]) + self.assertIn("41 years old", result[0]) + self.assertIn("6 foot 2 tall", result[0]) + self.assertIn("exactly one named character: Dave", result[0]) + self.assertNotIn("exactly 2 named characters", result[0]) + def test_h3_prompt_curator_defaults_anatomy_guard_to_on(self): required = self.image_nodes.DumasH3PromptCuratorNode.INPUT_TYPES()["required"]