MiniMax H3 Director: add frame_rate output and drop external_prompt input

- Add frame_rate FLOAT input (0.1-240, default 24) directly under duration
- Emit frame_rate as the final return output
- Remove external_prompt optional input; keep only external_prompt_overwrite
This commit is contained in:
Darksidewalker
2026-08-16 14:32:12 +02:00
parent 9cfb060473
commit 3daa486cd6
2 changed files with 53 additions and 12 deletions
+41 -3
View File
@@ -86,7 +86,7 @@ def test_director_emits_v2_consolidated_prompt_for_i2va_builder_state():
builder = default_builder_state("I2VA")
builder.update({"imd": "A bright room.", "soundscape": "birds", "music": "N/A"})
guide, _, resolved, _, _, _, fl2va_requested, ref2va_requested = director.MiniMaxH3Director().build_guide(
guide, _, resolved, _, _, _, fl2va_requested, ref2va_requested, _ = director.MiniMaxH3Director().build_guide(
"I2VA", "legacy", 1344, 768, 5, "match", json.dumps(state), json.dumps(builder)
)
@@ -103,10 +103,10 @@ def test_director_blank_external_prompt_falls_back_to_builder():
builder = default_builder_state("T2VA")
builder.update({"imd": "[Shot 1] A calm lake at dawn.", "soundscape": "light wind", "music": "N/A"})
def resolved_for(external_prompt):
def resolved_for(external_prompt_overwrite):
return director.MiniMaxH3Director().build_guide(
"T2VA", "", 1344, 768, 5, "match", json.dumps({"items": []}),
json.dumps(builder), None, None, external_prompt=external_prompt
json.dumps(builder), None, None, external_prompt_overwrite=external_prompt_overwrite
)[2]
# Blank string and whitespace-only must both resolve via the builder (non-empty).
@@ -293,6 +293,44 @@ def test_director_uses_the_native_h3_frame_grid_for_guide_and_output_length():
assert output_length == 124
def test_director_exposes_frame_rate_output_and_validates_range():
node = director.MiniMaxH3Director()
# Default frame_rate is 24 and is emitted as the final output.
*_, default_fps = node.build_guide("FL2VA", "", 1344, 768, 5, "match", "{}")
assert default_fps == 24.0
assert isinstance(default_fps, float)
# A selected frame_rate passes straight through to the output.
*_, chosen_fps = node.build_guide("FL2VA", "", 1344, 768, 5, "match", "{}", frame_rate=60)
assert chosen_fps == 60.0
# Fractional rates pass through, while the declared bounds are enforced.
*_, fractional = node.build_guide("FL2VA", "", 1344, 768, 5, "match", "{}", frame_rate=23.976)
assert fractional == 23.976
*_, low = node.build_guide("FL2VA", "", 1344, 768, 5, "match", "{}", frame_rate=0.1)
assert low == 0.1
*_, high = node.build_guide("FL2VA", "", 1344, 768, 5, "match", "{}", frame_rate=240)
assert high == 240.0
for out_of_range in (0.09, 240.01):
with pytest.raises(ValueError, match="frame_rate must be between 0.1 and 240"):
node.build_guide("FL2VA", "", 1344, 768, 5, "match", "{}", frame_rate=out_of_range)
def test_director_input_types_declare_frame_rate_directly_under_duration():
required = director.MiniMaxH3Director.INPUT_TYPES()["required"]
assert required["frame_rate"] == ("FLOAT", {"default": 24.0, "min": 0.1, "max": 240.0, "step": 0.01})
keys = list(required)
# The widget must render directly beneath duration in the node UI.
assert keys.index("frame_rate") == keys.index("duration") + 1
def test_director_keeps_only_external_prompt_overwrite_as_its_prompt_input():
optional = director.MiniMaxH3Director.INPUT_TYPES()["optional"]
assert "external_prompt_overwrite" in optional
assert "external_prompt" not in optional
def test_fl2va_slot_two_without_slot_one_is_the_closing_frame():
closing_frame = "closing.png"
state = {"items": [{
+12 -9
View File
@@ -31,6 +31,7 @@ class MiniMaxH3Director:
"width": ("INT", {"default": 1344, "min": 16, "max": 8192, "step": 16}),
"height": ("INT", {"default": 768, "min": 16, "max": 8192, "step": 16}),
"duration": ("INT", {"default": 5, "min": 1, "max": 1000}),
"frame_rate": ("FLOAT", {"default": 24.0, "min": 0.1, "max": 240.0, "step": 0.01}),
"ref_image_size": (["match", "max"], {"default": "match"}),
"timeline_data": ("STRING", {"default": "{\"version\":1,\"items\":[],\"prompt_blocks\":[]}", "multiline": False, "hidden": True}),
"builder_state": ("STRING", {"default": "", "multiline": False, "hidden": True}),
@@ -44,21 +45,21 @@ class MiniMaxH3Director:
},
}
RETURN_TYPES = ("MINIMAX_H3_DIRECTOR_GUIDE", "INT", "STRING", "INT", "INT", "MODEL", "BOOLEAN", "BOOLEAN")
RETURN_NAMES = ("guide", "duration", "positive_prompt", "width", "height", "model", "fl2va_requested", "ref2va_requested")
RETURN_TYPES = ("MINIMAX_H3_DIRECTOR_GUIDE", "INT", "STRING", "INT", "INT", "MODEL", "BOOLEAN", "BOOLEAN", "FLOAT")
RETURN_NAMES = ("guide", "duration", "positive_prompt", "width", "height", "model", "fl2va_requested", "ref2va_requested", "frame_rate")
FUNCTION = "build_guide"
CATEGORY = "DaSiWa Nodes/MiniMax H3"
def check_lazy_status(self, mode, prompt, width, height, duration, ref_image_size, timeline_data, builder_state,
fl2va_model=None, ref2va_model=None, external_width_overwrite=None,
external_height_overwrite=None, external_prompt_overwrite=None, external_prompt=None):
external_height_overwrite=None, external_prompt_overwrite=None, frame_rate=24.0):
selected_name = "ref2va_model" if mode == "REF2VA" else "fl2va_model"
selected_model = ref2va_model if mode == "REF2VA" else fl2va_model
return [selected_name] if selected_model is None else []
def build_guide(self, mode, prompt, width, height, duration, ref_image_size, timeline_data, builder_state="",
fl2va_model=None, ref2va_model=None, external_width_overwrite=None,
external_height_overwrite=None, external_prompt_overwrite=None, external_prompt=None):
external_height_overwrite=None, external_prompt_overwrite=None, frame_rate=24.0):
# Preserve direct Python callers that used the pre-builder positional model argument.
if builder_state is not None and not isinstance(builder_state, str):
if fl2va_model is None:
@@ -68,6 +69,9 @@ class MiniMaxH3Director:
raise ValueError("builder_state must be JSON text")
if mode not in BASE_MODES | {"REF2VA"}:
raise ValueError(f"unsupported MiniMax Director mode: {mode}")
frame_rate = float(frame_rate)
if not 0.1 <= frame_rate <= 240.0:
raise ValueError("MiniMax Director frame_rate must be between 0.1 and 240")
external_canvas = external_width_overwrite is not None or external_height_overwrite is not None
if external_canvas:
if external_width_overwrite is None or external_height_overwrite is None:
@@ -173,9 +177,8 @@ class MiniMaxH3Director:
validate_reference_limits(images=images, videos=videos, audios=audios)
blocks = state.get("prompt_blocks", [])
prompt_overwrite = external_prompt_overwrite if external_prompt_overwrite is not None else external_prompt
if isinstance(prompt_overwrite, str) and prompt_overwrite.strip():
resolved = prompt_overwrite
if isinstance(external_prompt_overwrite, str) and external_prompt_overwrite.strip():
resolved = external_prompt_overwrite
else:
resolved = build_prompt(merged)
if not any(str(merged.get(key) or "").strip() for key in ("imd", "soundscape")) and mode != "REF2VA":
@@ -192,8 +195,8 @@ class MiniMaxH3Director:
}
normalize_guide(guide)
selected_model = ref2va_model if mode == "REF2VA" else fl2va_model
log_dasiwa("MiniMax H3 Director", f"mode={mode}; requested_model={'ref2va_model' if mode == 'REF2VA' else 'fl2va_model'}; passed_model={_describe_model(selected_model)}; canvas={width}x{height}; frames={length}; refs=images:{len(ref_images)},videos:{len(ref_videos)},video_audio:{len(ref_video_audios)},audio:{len(ref_audios)}; timeline_items={len(items)}")
return guide, length, resolved, int(width), int(height), selected_model, mode in BASE_MODES, mode == "REF2VA"
log_dasiwa("MiniMax H3 Director", f"mode={mode}; requested_model={'ref2va_model' if mode == 'REF2VA' else 'fl2va_model'}; passed_model={_describe_model(selected_model)}; canvas={width}x{height}; frames={length}; fps={frame_rate}; refs=images:{len(ref_images)},videos:{len(ref_videos)},video_audio:{len(ref_video_audios)},audio:{len(ref_audios)}; timeline_items={len(items)}")
return guide, length, resolved, int(width), int(height), selected_model, mode in BASE_MODES, mode == "REF2VA", frame_rate
NODE_CLASS_MAPPINGS = {"MiniMaxH3Director": MiniMaxH3Director}