diff --git a/nodes/custom.py b/nodes/custom.py index ac5fad3..577272c 100644 --- a/nodes/custom.py +++ b/nodes/custom.py @@ -27,6 +27,7 @@ class MidjourneyCustomRequest(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), *IMAGINE_SUBMISSION_BINDINGS, bind("style_references"), bind("character_references"), diff --git a/nodes/midjourney_versions.py b/nodes/midjourney_versions.py index 21c4ac8..2620413 100644 --- a/nodes/midjourney_versions.py +++ b/nodes/midjourney_versions.py @@ -51,6 +51,7 @@ class MidjourneyV4Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), *IMAGINE_SUBMISSION_BINDINGS, bind("translate_weights"), bind("custom_args"), @@ -111,6 +112,7 @@ class MidjourneyV5Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), bind("use_raw"), *IMAGINE_SUBMISSION_BINDINGS, bind("translate_weights"), @@ -171,6 +173,7 @@ class MidjourneyV6Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), bind("use_raw"), *IMAGINE_SUBMISSION_BINDINGS, bind("translate_weights"), @@ -233,6 +236,7 @@ class MidjourneyV7Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), bind("use_raw"), bind("custom_args"), ), @@ -281,6 +285,7 @@ class MidjourneyV8AlphaRequest(PromptDrivenNode): bind("aspect_ratio"), bind("batch"), bind("seed"), + bind("send_explicit_seed"), bind("custom_args"), ), image_capability_target="midjourney_v8_alpha", diff --git a/nodes/niji.py b/nodes/niji.py index c74be06..0cc7da3 100644 --- a/nodes/niji.py +++ b/nodes/niji.py @@ -28,6 +28,7 @@ class Niji5Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), bind("use_raw"), *IMAGINE_SUBMISSION_BINDINGS, bind("translate_weights"), @@ -88,6 +89,7 @@ class Niji4Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), *IMAGINE_SUBMISSION_BINDINGS, bind("translate_weights"), bind("custom_args"), @@ -144,6 +146,7 @@ class Niji6Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), bind("use_raw"), *IMAGINE_SUBMISSION_BINDINGS, bind("translate_weights"), @@ -198,6 +201,7 @@ class Niji7Request(PromptDrivenNode): bind("batch"), bind("tile"), bind("seed"), + bind("send_explicit_seed"), bind("use_raw"), *IMAGINE_SUBMISSION_BINDINGS, bind("custom_args"), diff --git a/prompting/input_specs.py b/prompting/input_specs.py index ea2bd5d..a981a3b 100644 --- a/prompting/input_specs.py +++ b/prompting/input_specs.py @@ -212,6 +212,8 @@ def _render_batch_repeat(value: Any, _: NormalizedValues) -> str: def _render_seed(value: Any, _: NormalizedValues) -> str: """Render the seed flag when the seed is a non-negative integer.""" + if not _.get("send_explicit_seed"): + return "" coerced_value = coerce_int(value) return ( f" --seed {value}" if coerced_value is not None and coerced_value >= 0 else "" @@ -455,11 +457,31 @@ INPUT_SPECS: dict[str, InputSpec] = { "min": 0, "max": 4294967295, "step": 1, - "tooltip": "Random seed for reproducibility (--seed)", + "tooltip": ( + "Optional Midjourney seed value. Midjourney randomizes by " + 'default when no seed is sent. Enable "Send Explicit Seed" to ' + "include this value as --seed." + ), }, role=InputRole.PROMPT_FLAG, renderer=_render_seed, ), + "send_explicit_seed": InputSpec( + key="send_explicit_seed", + field_name="send_explicit_seed", + field_type="BOOLEAN", + field_options={ + "default": False, + "label_on": "Send Explicit Seed", + "label_off": "Send Explicit Seed", + "tooltip": ( + "When enabled, Mutiny sends the Seed value to Midjourney as " + "--seed. Leave disabled to use Midjourney's default randomized " + "seed behavior." + ), + }, + role=InputRole.PROMPT_FLAG, + ), "use_raw": InputSpec( key="use_raw", field_name="use_raw", diff --git a/tests/nodes/test_execution.py b/tests/nodes/test_execution.py index 4e315ce..6f446dd 100644 --- a/tests/nodes/test_execution.py +++ b/tests/nodes/test_execution.py @@ -228,6 +228,24 @@ def test_midjourney_v7_node_submits_prompt_images_through_runtime( assert len(image_inputs.prompt_images) == 1 assert image_inputs.prompt_images[0].startswith("data:image/png;base64,") + +def test_midjourney_v7_node_sends_seed_only_when_explicitly_enabled( + plugin_package, make_node, fake_runtime_service_factory +): + """Submit ``--seed`` only when the explicit seed toggle is enabled.""" + node_class = plugin_package.NODE_CLASS_MAPPINGS["MidjourneyV7Request"] + fake_runtime = fake_runtime_service_factory() + node = make_node(node_class, _runtime_service=fake_runtime) + + node.generate( + prompt="cat", + seed=42, + send_explicit_seed=True, + ) + + assert fake_runtime.imagine_calls[0]["prompt_text"] == "cat --seed 42 --v 7" + + def test_niji6_node_submits_batched_images_through_runtime( plugin_package, make_node, fake_runtime_service_factory ): diff --git a/tests/prompting/test_input_specs.py b/tests/prompting/test_input_specs.py index 2cc5fd4..eeae596 100644 --- a/tests/prompting/test_input_specs.py +++ b/tests/prompting/test_input_specs.py @@ -92,6 +92,28 @@ def test_stylize_spec_omits_default_flag(plugin_package): assert renderer(250, {}) == " --stylize 250" +def test_seed_spec_requires_explicit_send_toggle(plugin_package): + """Render the seed flag only when explicit seed sending is enabled.""" + specs_module = sys.modules[f"{plugin_package.__name__}.prompting.input_specs"] + renderer = specs_module.INPUT_SPECS["seed"].renderer + + assert renderer is not None + assert renderer(42, {"send_explicit_seed": False}) == "" + assert renderer(42, {"send_explicit_seed": True}) == " --seed 42" + + +def test_send_explicit_seed_spec_declares_boolean_default(plugin_package): + """Expose the explicit-seed toggle with the approved UI contract.""" + specs_module = sys.modules[f"{plugin_package.__name__}.prompting.input_specs"] + seed_toggle_spec = specs_module.INPUT_SPECS["send_explicit_seed"] + + assert seed_toggle_spec.field_name == "send_explicit_seed" + assert seed_toggle_spec.field_type == "BOOLEAN" + assert seed_toggle_spec.field_options["default"] is False + assert seed_toggle_spec.field_options["label_on"] == "Send Explicit Seed" + assert seed_toggle_spec.field_options["label_off"] == "Send Explicit Seed" + + def test_use_raw_spec_renders_raw_mode_flag(plugin_package): """Render the shared Raw Mode toggle as ``--raw`` for all supported nodes.""" specs_module = sys.modules[f"{plugin_package.__name__}.prompting.input_specs"] diff --git a/tests/prompting/test_prompt_builder.py b/tests/prompting/test_prompt_builder.py index d95d664..e148611 100644 --- a/tests/prompting/test_prompt_builder.py +++ b/tests/prompting/test_prompt_builder.py @@ -31,7 +31,7 @@ def test_custom_request_prompt_builder_fixes_version_spacing(plugin_package): ) assert build_result.prompt_text == ( - "cat::1.4 --v 7 --no dog::0.5 --raw --chaos 5 --repeat 2 --tile --seed 9" + "cat::1.4 --v 7 --no dog::0.5 --raw --chaos 5 --repeat 2 --tile" ) assert build_result.build_controls["translate_weights"] is True assert build_result.submission_controls == { @@ -86,7 +86,7 @@ def test_v7_prompt_builder_renders_wrapped_image_metadata(plugin_package): assert build_result.prompt_text == ( "cat --no dog --stylize 250 --chaos 7 --weird 12 --p p123 --ar 16:9" - " --q 2 --exp 5 --repeat 3 --tile --seed 42 --raw --stylize 900" + " --q 2 --exp 5 --repeat 3 --tile --raw --stylize 900" " --iw 1.5 --sw 0 --sv 5 --ow 321" ) assert build_result.submission_controls["images"] is wrapped_prompt_images @@ -113,6 +113,24 @@ def test_v7_prompt_builder_renders_bare_personalization(plugin_package): assert build_result.prompt_text == "cat --p" + +def test_v7_prompt_builder_sends_seed_only_when_explicitly_enabled(plugin_package): + """Render ``--seed`` only when the explicit seed toggle is enabled.""" + prompting_module = sys.modules[f"{plugin_package.__name__}.prompting"] + node_class = plugin_package.NODE_CLASS_MAPPINGS["MidjourneyV7Request"] + + build_result = prompting_module.build_prompt( + node_class.NODE_DEFINITION, + { + "prompt": "cat", + "seed": 42, + "send_explicit_seed": True, + }, + ) + + assert build_result.prompt_text == "cat --seed 42" + + def test_v7_prompt_builder_rejects_conflicting_personalization_controls( plugin_package, ): @@ -231,7 +249,7 @@ def test_niji7_prompt_builder_appends_niji7_supported_flags(plugin_package): assert build_result.prompt_text == ( "cat --no dog --chaos 5 --weird 20 --ar 2:1" - " --repeat 2 --tile --seed 8 --raw --chaos 9 --sw 0 --sv 4" + " --repeat 2 --tile --raw --chaos 9 --sw 0 --sv 4" ) diff --git a/tests/test_exports.py b/tests/test_exports.py index 585d57e..ede4973 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -122,6 +122,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "images", "style_references", "character_references", @@ -144,6 +145,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "images", "translate_weights", "custom_args", @@ -165,6 +167,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "use_raw", "images", "translate_weights", @@ -190,6 +193,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "use_raw", "images", "translate_weights", @@ -218,6 +222,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "use_raw", "custom_args", ], @@ -234,6 +239,7 @@ EXPECTED_NODE_METADATA = { "aspect_ratio", "batch", "seed", + "send_explicit_seed", "custom_args", ], "return_types": ("IMAGE",), @@ -253,6 +259,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "images", "translate_weights", "custom_args", @@ -275,6 +282,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "use_raw", "images", "translate_weights", @@ -300,6 +308,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "use_raw", "images", "translate_weights", @@ -322,6 +331,7 @@ EXPECTED_NODE_METADATA = { "batch", "tile", "seed", + "send_explicit_seed", "use_raw", "images", "custom_args",