Compare commits

..
2 Commits
Author SHA1 Message Date
chris.dumas 7de62226f2 Strip legacy anchor style note 2026-09-07 08:01:22 +00:00
chris.dumas a51141cb26 Add location helper reference outputs 2026-09-07 07:46:24 +00:00
3 changed files with 97 additions and 9 deletions
+4 -3
View File
@@ -91,8 +91,9 @@
- `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.
- Outputs: `image1`, `image2`, `reference_prompt`, `reference1`, `reference2`
- Matching general-purpose helper for environments/locations: pass two images through unchanged, emit location reference prompt text, and provide two structured `REFERENCE` objects for the prompt curator.
- The structured references carry the same location name, alias, description, and notes, so mentioning the location name in `Dumas H3 Prompt Curator` can include both helper images and the location context automatically.
- `Dumas Anchor Style`
- Inputs: `anchor_style`, `style_description`
@@ -262,7 +263,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 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 want a single `REFERENCE` socket. `Dumas H3 Prompt Curator` consumes those structured references, assigns the final `<Picture N>` numbering, and outputs only the compacted images the prompt actually mentions.
`Dumas Character Helper` is the restored two-image/text helper for general H3 workflows, and `Dumas Location Helper` mirrors it for scene/environment references. Both helpers also emit structured `REFERENCE` sockets for the curator. The structured `Dumas Character Reference` and `Dumas Location Reference` nodes remain available separately for workflows that want a single `REFERENCE` socket. `Dumas H3 Prompt Curator` consumes those structured references, assigns the final `<Picture N>` numbering, and outputs only the compacted images the prompt actually mentions.
`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.
+35 -5
View File
@@ -27,6 +27,12 @@ _H3_PLAN_IMAGE_BINDINGS_CAP = 128
_H3_PLAN_IMAGE_SLOTS = 9
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
_ANCHOR_STYLE_H3_NOTE = ""
_ANCHOR_STYLE_LEGACY_NOTE_RE = re.compile(
r"\s*Keep this anchor focused on persistent camera language, lighting, "
r"texture, environment treatment, and tone; do not name characters or "
r"describe one-off actions\.?",
re.I,
)
_H3_PROMPT_REF_SLOTS = 9
_H3_PROMPT_MAX_CHARS = 7000
_PICTURE_TAG_RE = re.compile(r"<\s*picture[\s_\-]*(\d+)\s*>", re.I)
@@ -420,6 +426,11 @@ def _anchor_style_description(style_name):
return _ANCHOR_STYLE_PRESETS.get(str(style_name or "").strip().lower(), "")
def _clean_anchor_style_text(text):
cleaned = _ANCHOR_STYLE_LEGACY_NOTE_RE.sub("", str(text or ""))
return re.sub(r"[ \t]{2,}", " ", cleaned).strip()
def _clean_input_token_value(value):
cleaned = ""
if value is not None:
@@ -2164,8 +2175,8 @@ class DumasLocationHelperNode:
"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")
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", _REFERENCE_TYPE, _REFERENCE_TYPE)
RETURN_NAMES = ("image1", "image2", "reference_prompt", "reference1", "reference2")
FUNCTION = "build_location_text"
CATEGORY = "Dumas/MiniMax"
@@ -2253,7 +2264,26 @@ class DumasLocationHelperNode:
description,
general,
)
return (image1, image2, text)
common = {
"kind": "location",
"explicit_id": location_id,
"name": name,
"aliases": alias,
"description": description,
"general": general,
"facts": {},
}
reference1 = make_reference(
image=image1,
summary="Primary location reference.",
**common,
)
reference2 = make_reference(
image=image2,
summary="Secondary location reference.",
**common,
)
return (image1, image2, text, reference1, reference2)
class DumasCharacterReferenceNode:
@@ -2685,9 +2715,9 @@ class DumasAnchorStyleNode:
}
def build_anchor(self, anchor_style, style_description):
text = str(style_description or "").strip()
text = _clean_anchor_style_text(style_description)
if not text:
text = _anchor_style_description(anchor_style)
text = _clean_anchor_style_text(_anchor_style_description(anchor_style))
return (text,)
+58 -1
View File
@@ -421,7 +421,17 @@ class DumasImageNodeTests(unittest.TestCase):
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)
self.assertIs(result[3]["image"], image1)
self.assertIs(result[4]["image"], image2)
self.assertEqual(result[3]["kind"], "location")
self.assertEqual(result[4]["kind"], "location")
self.assertEqual(result[3]["id"], "coffee-shop-01")
self.assertEqual(result[4]["id"], "coffee-shop-01")
self.assertEqual(result[3]["name"], "Coffee Shop")
self.assertEqual(result[3]["aliases"], ["Cafe Interior"])
self.assertEqual(result[3]["description"], "Warm tungsten lighting, narrow counter, rainy front window")
self.assertEqual(result[3]["general"], "Evening ambience, cramped but cozy")
self.assertEqual(len(result), 5)
def test_soundscape_helper_defaults_to_selected_preset_description(self):
node = self.image_nodes.DumasSoundscapeHelperNode()
@@ -560,6 +570,40 @@ class DumasImageNodeTests(unittest.TestCase):
self.assertIn("exactly one named character: <Picture 1> Dave", result[0])
self.assertNotIn("exactly 2 named characters", result[0])
def test_h3_prompt_curator_uses_location_helper_references_by_name(self):
helper = self.image_nodes.DumasLocationHelperNode()
curator = self.image_nodes.DumasH3PromptCuratorNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
helper_result = helper.build_location_text(
image1=image1,
image2=image2,
image1_picture_id="1",
image2_picture_id="2",
location_id="coffee_shop",
name="Coffee Shop",
alias="Cafe Interior",
description="Warm tungsten lighting, narrow counter, rainy front window",
general="Evening ambience, cramped but cozy",
)
result = curator.curate_prompt(
action_prompt="A slow push through the Coffee Shop as rain streaks the windows.",
anatomy_guard="on",
subject_count_guard="auto",
ref_1=helper_result[3],
ref_2=helper_result[4],
)
self.assertIs(result[1], image1)
self.assertIs(result[2], image2)
self.assertEqual(result[10], 2)
self.assertIn("<Picture 1> Coffee Shop", result[0])
self.assertIn("<Picture 2> Coffee Shop", result[0])
self.assertIn("Location context for <Picture 1> Coffee Shop", result[0])
self.assertIn("Warm tungsten lighting", result[0])
self.assertNotIn("Subject count guard:", result[0])
def test_h3_prompt_curator_defaults_anatomy_guard_to_on(self):
required = self.image_nodes.DumasH3PromptCuratorNode.INPUT_TYPES()["required"]
@@ -644,6 +688,19 @@ class DumasImageNodeTests(unittest.TestCase):
self.assertIn("real time", result[0])
self.assertNotIn("persistent camera language", result[0])
def test_anchor_style_node_strips_legacy_persistent_anchor_note(self):
node = self.image_nodes.DumasAnchorStyleNode()
legacy = (
"Gritty handheld realism. Keep this anchor focused on persistent camera "
"language, lighting, texture, environment treatment, and tone; do not "
"name characters or describe one-off actions."
)
result = node.build_anchor("cinematic action movie", legacy)
self.assertEqual(result[0], "Gritty handheld realism.")
self.assertNotIn("persistent camera language", result[0])
def test_anchor_style_node_prefers_manual_description_edits(self):
node = self.image_nodes.DumasAnchorStyleNode()
custom = "Lo-fi pirate broadcast with smeared highlights and anxious zoom corrections."