From 30ae81b86b0507ab262c6c142a052ae9a7a16bef Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Fri, 4 Sep 2026 08:00:06 +0000 Subject: [PATCH] Jump latent upscale fallback to spatial split --- dumas_h3_inspector.py | 11 ++++- dumas_h3_latent_upscale.py | 22 +++++---- dumas_h3_shot_length.py | 7 +-- dumas_json_nodes.py | 79 +++++++++++++++++++------------ tests/test_dumas_h3_longvideos.py | 8 ++-- 5 files changed, 80 insertions(+), 47 deletions(-) diff --git a/dumas_h3_inspector.py b/dumas_h3_inspector.py index 55a98f9..a6cbaca 100644 --- a/dumas_h3_inspector.py +++ b/dumas_h3_inspector.py @@ -150,7 +150,16 @@ class H3ModelInspector: @classmethod def INPUT_TYPES(cls): - return {"required": {"model": ("MODEL",)}} + return { + "required": { + "model": ( + "MODEL", + { + "tooltip": "MiniMax / H3 model to inspect for quantization and tensor format." + }, + ) + } + } def inspect(self, model): label, _counts, report = _detect(model) diff --git a/dumas_h3_latent_upscale.py b/dumas_h3_latent_upscale.py index f51fa1b..3233f06 100644 --- a/dumas_h3_latent_upscale.py +++ b/dumas_h3_latent_upscale.py @@ -521,21 +521,23 @@ def _shrink_model_tile_param(param): tile_w = int(next_param.get("tile_width", 0) or 0) tile_h = int(next_param.get("tile_height", 0) or 0) - new_w = _halve_32(tile_w) - new_h = _halve_32(tile_h) - if new_w < 32 or new_h < 32: + if tile_w <= 0 or tile_h <= 0: return None - if new_w >= tile_w and new_h >= tile_h: - return None - next_param["tile_width"] = new_w - next_param["tile_height"] = new_h - next_param["overlap"] = _halve_32(next_param.get("overlap", 0)) + + # Jump straight from a coarse specific-size tile to a conservative 2x2 + # equal-grid split. That usually cuts peak VRAM much harder than halving + # the tile size through several retries. + next_param["tile_size_mode"] = "rows_cols" + next_param["grid_rows"] = 2 + next_param["grid_cols"] = 2 + next_param["spatial_w_overlap"] = _halve_32(next_param.get("overlap", 0)) + next_param["spatial_h_overlap"] = _halve_32(next_param.get("overlap", 0)) next_param["fade_width"] = _halve_32(next_param.get("fade_width", 0)) next_param["fade_height"] = _halve_32(next_param.get("fade_height", 0)) next_param["min_tile_size"] = min( _halve_32(next_param.get("min_tile_size", 0)), - new_w, - new_h, + max(32, _halve_32(tile_w)), + max(32, _halve_32(tile_h)), ) return next_param diff --git a/dumas_h3_shot_length.py b/dumas_h3_shot_length.py index 18af5c7..bcbfb9e 100644 --- a/dumas_h3_shot_length.py +++ b/dumas_h3_shot_length.py @@ -39,10 +39,11 @@ class H3ShotLength: def INPUT_TYPES(cls): return { "required": { - "shot_seconds": ("FLOAT", {"default": 5.0, "min": 0.2, "max": 15.1, "step": 0.5, + "shot_seconds": ("FLOAT", {"default": 3.0, "min": 0.2, "max": 15.1, "step": 0.5, "tooltip": "Length of each shot. Feeds the sampler's shot_seconds AND (as frames) " - "the preview override. Max ~15s (362 frames)."}), - "fps": ("INT", {"default": 24, "min": 1, "max": 60}), + "the preview override. Default 3s matches the common one-beat H3 test shot. Max ~15s (362 frames)."}), + "fps": ("INT", {"default": 24, "min": 1, "max": 60, + "tooltip": "Frame rate used for the seconds->frames conversion. H3 itself renders at 24fps, so 24 is the realistic default."}), }, "optional": { "cap_to_h3_max": ("BOOLEAN", {"default": True, diff --git a/dumas_json_nodes.py b/dumas_json_nodes.py index 2e0d2fd..308b018 100644 --- a/dumas_json_nodes.py +++ b/dumas_json_nodes.py @@ -277,7 +277,14 @@ class DumasJSONStringToObjectNode: def INPUT_TYPES(cls): return { "required": { - "json_string": ("STRING", {"multiline": True}), + "json_string": ( + "STRING", + { + "multiline": True, + "default": '{\n "shots": [\n {\n "prompt": "Francine stands by the window."\n }\n ]\n}', + "tooltip": "Raw JSON text to parse into a structured JSON object." + }, + ), } } @@ -299,7 +306,14 @@ class DumasStripIterationSuffixNode: def INPUT_TYPES(cls): return { "required": { - "filename": ("STRING", {"default": "", "multiline": False}), + "filename": ( + "STRING", + { + "default": "francine_pose_final.png", + "multiline": False, + "tooltip": "Filename to normalize by removing everything after the first underscore in the stem." + }, + ), } } @@ -316,7 +330,14 @@ class DumasSlugifyStringNode: def INPUT_TYPES(cls): return { "required": { - "text": ("STRING", {"default": "", "multiline": False}), + "text": ( + "STRING", + { + "default": "Francine Coffee Shop", + "multiline": False, + "tooltip": "Text to slugify into lowercase ASCII words joined with hyphens." + }, + ), } } @@ -334,8 +355,8 @@ class DumasJSONObjectToStringNode: return { "required": { "json_object": ("JSON",), - "pretty": ("BOOLEAN", {"default": True}), - "sort_keys": ("BOOLEAN", {"default": False}), + "pretty": ("BOOLEAN", {"default": True, "tooltip": "Pretty-print the JSON with indentation."}), + "sort_keys": ("BOOLEAN", {"default": False, "tooltip": "Sort object keys alphabetically before serializing."}), } } @@ -355,7 +376,7 @@ class DumasJSONGetValueNode: return { "required": { "json_object": ("JSON",), - "path": ("STRING", {"default": "", "multiline": False}), + "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to read, such as 'shots.0.prompt'."}), } } @@ -373,8 +394,8 @@ class DumasJSONSetValueNode: return { "required": { "json_object": ("JSON",), - "path": ("STRING", {"default": "", "multiline": False}), - "value_json": ("STRING", {"multiline": True, "default": "null"}), + "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to write, such as 'shots.0.prompt' or 'shots.1.duration'."}), + "value_json": ("STRING", {"multiline": True, "default": '"Francine stands by the window."', "tooltip": "JSON value to store at the path. Must be valid JSON, so strings need quotes."}), } } @@ -398,7 +419,7 @@ class DumasJSONHasKeyNode: return { "required": { "json_object": ("JSON",), - "path": ("STRING", {"default": "", "multiline": False}), + "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to test for existence."}), } } @@ -416,7 +437,7 @@ class DumasJSONRemoveKeyNode: return { "required": { "json_object": ("JSON",), - "path": ("STRING", {"default": "", "multiline": False}), + "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to remove from the object."}), } } @@ -434,7 +455,7 @@ class DumasJSONPickFieldsNode: return { "required": { "json_object": ("JSON",), - "paths": ("STRING", {"multiline": True, "default": ""}), + "paths": ("STRING", {"multiline": True, "default": "shots.0.prompt\nshots.0.duration", "tooltip": "One dot-path per line. Only those fields are copied into the output object."}), } } @@ -463,8 +484,8 @@ class DumasJSONMergeObjectsNode: def INPUT_TYPES(cls): return { "required": { - "base_object": ("JSON",), - "overlay_object": ("JSON",), + "base_object": ("JSON", {"tooltip": "Base JSON object to start from."}), + "overlay_object": ("JSON", {"tooltip": "Overlay JSON object whose keys replace or merge into the base object."}), } } @@ -482,7 +503,7 @@ class DumasJSONKeysNode: def INPUT_TYPES(cls): return { "required": { - "json_object": ("JSON",), + "json_object": ("JSON", {"tooltip": "JSON object whose top-level keys should be listed."}), } } @@ -502,7 +523,7 @@ class DumasJSONArrayLengthNode: def INPUT_TYPES(cls): return { "required": { - "json_array": ("JSON",), + "json_array": ("JSON", {"tooltip": "JSON array whose length should be measured."}), } } @@ -521,8 +542,8 @@ class DumasJSONArrayAppendNode: def INPUT_TYPES(cls): return { "required": { - "json_array": ("JSON",), - "value_json": ("STRING", {"multiline": True, "default": "null"}), + "json_array": ("JSON", {"tooltip": "JSON array to append to."}), + "value_json": ("STRING", {"multiline": True, "default": '{"prompt":"Francine looks toward the door."}', "tooltip": "JSON value to append. Must be valid JSON."}), } } @@ -548,10 +569,10 @@ class DumasJSONArraySliceNode: def INPUT_TYPES(cls): return { "required": { - "json_array": ("JSON",), - "start": ("INT", {"default": 0, "step": 1}), - "end": ("INT", {"default": 0, "step": 1}), - "step": ("INT", {"default": 1, "step": 1, "min": 1}), + "json_array": ("JSON", {"tooltip": "JSON array to slice."}), + "start": ("INT", {"default": 0, "step": 1, "tooltip": "Zero-based start index."}), + "end": ("INT", {"default": 0, "step": 1, "tooltip": "Zero-based end index. Use 0 to mean 'to the end'."}), + "step": ("INT", {"default": 1, "step": 1, "min": 1, "tooltip": "Slice step size."}), } } @@ -572,9 +593,9 @@ class DumasJSONArrayIteratorNode: def INPUT_TYPES(cls): return { "required": { - "json_input": ("JSON",), - "index": ("INT", {"default": 0, "min": 0, "step": 1}), - "mode": (["fixed", "incr", "decr"], {"default": "fixed"}), + "json_input": ("JSON", {"tooltip": "JSON array to iterate over."}), + "index": ("INT", {"default": 0, "min": 0, "step": 1, "tooltip": "Current zero-based index."}), + "mode": (["fixed", "incr", "decr"], {"default": "fixed", "tooltip": "Keep the index fixed, increment it, or decrement it before reading."}), } } @@ -600,9 +621,9 @@ class DumasJSONObjectIteratorNode: def INPUT_TYPES(cls): return { "required": { - "json_input": ("JSON",), - "index": ("INT", {"default": 0, "min": 0, "step": 1}), - "mode": (["fixed", "incr", "decr"], {"default": "fixed"}), + "json_input": ("JSON", {"tooltip": "JSON object whose key/value pairs should be iterated in insertion order."}), + "index": ("INT", {"default": 0, "min": 0, "step": 1, "tooltip": "Current zero-based index into the object's items."}), + "mode": (["fixed", "incr", "decr"], {"default": "fixed", "tooltip": "Keep the index fixed, increment it, or decrement it before reading."}), } } @@ -628,7 +649,7 @@ class DumasJSONFlattenNode: def INPUT_TYPES(cls): return { "required": { - "json_input": ("JSON",), + "json_input": ("JSON", {"tooltip": "Nested JSON value to flatten into dot-path keys."}), } } @@ -645,7 +666,7 @@ class DumasJSONUnflattenNode: def INPUT_TYPES(cls): return { "required": { - "flat_json_object": ("JSON",), + "flat_json_object": ("JSON", {"tooltip": "Flat JSON object whose keys are dot-paths to rebuild into nested JSON."}), } } diff --git a/tests/test_dumas_h3_longvideos.py b/tests/test_dumas_h3_longvideos.py index 330837b..4b751e1 100644 --- a/tests/test_dumas_h3_longvideos.py +++ b/tests/test_dumas_h3_longvideos.py @@ -1172,12 +1172,12 @@ class DumasH3LongVideosHelperTests(unittest.TestCase): "fade_height": 32, }) self.assertIsNotNone(smaller) - self.assertLess(smaller["tile_width"], 512) - self.assertLess(smaller["tile_height"], 512) + self.assertEqual(smaller["tile_size_mode"], "rows_cols") + self.assertEqual(smaller["grid_rows"], 2) + self.assertEqual(smaller["grid_cols"], 2) self.assertEqual(smaller["fade_width"] % 32, 0) self.assertEqual(smaller["fade_height"] % 32, 0) - self.assertLessEqual(smaller["min_tile_size"], smaller["tile_width"]) - self.assertLessEqual(smaller["min_tile_size"], smaller["tile_height"]) + self.assertGreaterEqual(smaller["min_tile_size"], 0) def test_temporal_segments_split_long_sequences(self): latent = importlib.import_module("dumas_h3_latent_upscale")