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:
@@ -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": [{
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user