diff --git a/README.md b/README.md index c7483e4..af457c4 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,15 @@ - Supports manual picks, `Select all`, `First N`, and `Select random` so each run can choose one random image from the visible folder tree. - `Dumas H3 Plan Attach Scene Images` - - Inputs: `plan`, `scene_index`, optional `image1`..`image7` + - Inputs: `plan`, `scene_index`, optional `image1`..`image9` - Outputs: `plan`, `connected_images` - - Attaches up to seven optional image sockets to one MiniMax H3 plan scene while keeping the plan JSON-serializable for the upstream archive system. - - Chain one node per scene that needs its own seven-image bundle. + - Attaches up to nine optional image sockets to one MiniMax H3 plan scene while keeping the plan JSON-serializable for the upstream archive system. + - Chain one node per scene that needs its own nine-image bundle. - `Dumas H3 Plan Extract Scene Images` - Inputs: `plan`, `scene_index` - - Outputs: `plan`, `image1`..`image7`, `connected_images` - - Reads back the seven optional images for a selected MiniMax H3 plan scene, for example by connecting the current `clip_index`. + - Outputs: `plan`, `image1`..`image9`, `connected_images` + - Reads back the nine optional images for a selected MiniMax H3 plan scene, for example by connecting the current `clip_index`. - `Dumas Character Helper` - Inputs: `image1`, `image2`, `image1_picture_id`, `image2_picture_id`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `general` @@ -193,7 +193,7 @@ decr -> use index - 1 `Save Image Dumas` leaves `folder` empty to use ComfyUI's output directory. `name` feeds `%input%` and `name_2` feeds `%input2%`, so a pattern like `project/%input%_%input2%_%counter%` can combine two upstream strings into the saved filename. -`Dumas H3 Plan Attach Scene Images` and `Dumas H3 Plan Extract Scene Images` are a companion pair for `ComfyUI-MiniMaxH3-Contex-Loop`. The upstream H3 plan node cannot dynamically grow six 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 six IMAGE sockets per scene through chained helper nodes. +`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` lives in `Dumas/String`. Use the picture ID dropdowns to decide which `` tags get mentioned in the generated text, while the two IMAGE sockets continue downstream unchanged. The node is tuned for useful non-visible facts rather than visual descriptions already obvious from the reference images. diff --git a/dumas_image_nodes.py b/dumas_image_nodes.py index 0a6d58a..a1d138a 100644 --- a/dumas_image_nodes.py +++ b/dumas_image_nodes.py @@ -23,6 +23,7 @@ _H3_PLAN_TYPE = "H3_CHAIN_PLAN" _H3_PLAN_IMAGE_BINDINGS_KEY = "_dumas_scene_image_bindings" _H3_PLAN_IMAGE_BINDINGS = OrderedDict() _H3_PLAN_IMAGE_BINDINGS_CAP = 128 +_H3_PLAN_IMAGE_SLOTS = 9 _FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif") _LOAD_IMAGES_FOLDER_DEFAULT_STATE = { "version": 1, @@ -192,8 +193,10 @@ def _scene_images_tuple( image5=None, image6=None, image7=None, + image8=None, + image9=None, ): - return (image1, image2, image3, image4, image5, image6, image7) + return (image1, image2, image3, image4, image5, image6, image7, image8, image9) def _connected_image_count(images): @@ -907,7 +910,7 @@ class DumasLoadImagesFolderNode: class DumasH3PlanAttachSceneImagesNode: DESCRIPTION = ( - "Attach up to seven optional IMAGE sockets to one H3 Chain Plan scene " + "Attach up to nine optional IMAGE sockets to one H3 Chain Plan scene " "without breaking the upstream plan archive format. Chain multiple " "copies of this node to bind different scene indexes." ) @@ -919,7 +922,7 @@ class DumasH3PlanAttachSceneImagesNode: @classmethod def INPUT_TYPES(cls): optional = {} - for slot in range(1, 8): + for slot in range(1, _H3_PLAN_IMAGE_SLOTS + 1): optional[f"image{slot}"] = ( "IMAGE", { @@ -948,7 +951,7 @@ class DumasH3PlanAttachSceneImagesNode: "step": 1, "tooltip": ( "1-based scene index inside the H3 plan. Use one node per " - "scene that needs up to seven image sockets." + "scene that needs up to nine image sockets." ), }, ), @@ -967,10 +970,22 @@ class DumasH3PlanAttachSceneImagesNode: image5=None, image6=None, image7=None, + image8=None, + image9=None, ): updated_plan = _clone_h3_plan(plan) scene_index = _normalize_h3_scene_index(updated_plan, scene_index) - images = _scene_images_tuple(image1, image2, image3, image4, image5, image6, image7) + images = _scene_images_tuple( + image1, + image2, + image3, + image4, + image5, + image6, + image7, + image8, + image9, + ) connected_count = _connected_image_count(images) token, scene_counts = _h3_plan_binding_entry(updated_plan) @@ -1001,7 +1016,7 @@ class DumasH3PlanAttachSceneImagesNode: class DumasH3PlanExtractSceneImagesNode: DESCRIPTION = ( - "Read back the seven optional image bindings for one H3 Chain Plan scene. " + "Read back the nine optional image bindings for one H3 Chain Plan scene. " "Connect clip_index or another scene selector to recover the matching " "scene images downstream." ) @@ -1014,6 +1029,8 @@ class DumasH3PlanExtractSceneImagesNode: "IMAGE", "IMAGE", "IMAGE", + "IMAGE", + "IMAGE", "INT", ) RETURN_NAMES = ( @@ -1025,6 +1042,8 @@ class DumasH3PlanExtractSceneImagesNode: "image5", "image6", "image7", + "image8", + "image9", "connected_images", ) FUNCTION = "extract" @@ -1063,11 +1082,11 @@ class DumasH3PlanExtractSceneImagesNode: scene_index = _normalize_h3_scene_index(passthrough_plan, scene_index) token, _scene_counts = _h3_plan_binding_entry(passthrough_plan) if not token: - return (passthrough_plan, None, None, None, None, None, None, None, 0) + return (passthrough_plan, *(None for _ in range(_H3_PLAN_IMAGE_SLOTS)), 0) registry = _H3_PLAN_IMAGE_BINDINGS.get(token) or {} _touch_plan_image_binding(token) - images = registry.get(int(scene_index)) or (None, None, None, None, None, None, None) + images = registry.get(int(scene_index)) or (None,) * _H3_PLAN_IMAGE_SLOTS return (passthrough_plan, *images, _connected_image_count(images)) diff --git a/tests/test_dumas_image_nodes.py b/tests/test_dumas_image_nodes.py index 5522fd1..98bf72a 100644 --- a/tests/test_dumas_image_nodes.py +++ b/tests/test_dumas_image_nodes.py @@ -524,6 +524,8 @@ class DumasImageNodeTests(unittest.TestCase): image_1 = FakeTensorBatch() image_2 = FakeTensorBatch(width=12, height=9) image_3 = FakeTensorBatch(width=8, height=8) + image_4 = FakeTensorBatch(width=16, height=16) + image_5 = FakeTensorBatch(width=20, height=12) plan_after_first, _connected = attach_node.attach(plan=plan, scene_index=1, image2=image_1) plan_after_second, _connected = attach_node.attach( @@ -531,6 +533,8 @@ class DumasImageNodeTests(unittest.TestCase): scene_index=2, image6=image_2, image7=image_3, + image8=image_4, + image9=image_5, ) scene1 = extract_node.extract(plan_after_second, 1) @@ -540,10 +544,12 @@ class DumasImageNodeTests(unittest.TestCase): self.assertEqual(scene1[-1], 1) self.assertIs(scene2[6], image_2) self.assertIs(scene2[7], image_3) - self.assertEqual(scene2[-1], 2) + self.assertIs(scene2[8], image_4) + self.assertIs(scene2[9], image_5) + self.assertEqual(scene2[-1], 4) self.assertEqual( plan_after_second["_dumas_scene_image_bindings"]["scene_counts"], - {"1": 1, "2": 2}, + {"1": 1, "2": 4}, ) def test_h3_plan_scene_images_metadata_is_json_serializable(self): @@ -559,6 +565,32 @@ class DumasImageNodeTests(unittest.TestCase): self.assertEqual(connected, 1) json.dumps(attached_plan) + def test_h3_plan_scene_images_support_nine_slots(self): + attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode() + extract_node = self.image_nodes.DumasH3PlanExtractSceneImagesNode() + plan = {"shots": [{"id": "one"}]} + images = [FakeTensorBatch(width=8 + index, height=8 + index) for index in range(9)] + + attached_plan, connected = attach_node.attach( + plan=plan, + scene_index=1, + image1=images[0], + image2=images[1], + image3=images[2], + image4=images[3], + image5=images[4], + image6=images[5], + image7=images[6], + image8=images[7], + image9=images[8], + ) + extracted = extract_node.extract(attached_plan, 1) + + self.assertEqual(connected, 9) + for index, image in enumerate(images, start=1): + self.assertIs(extracted[index], image) + self.assertEqual(extracted[-1], 9) + def test_h3_plan_scene_images_can_clear_a_scene_binding(self): attach_node = self.image_nodes.DumasH3PlanAttachSceneImagesNode() extract_node = self.image_nodes.DumasH3PlanExtractSceneImagesNode()