Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Shell completions cover every installed command**: the generator's hand-maintained list had drifted six commands behind pyproject (`mflux-generate-boogu`, `mflux-generate-lens`, both `mflux-generate-ernie-image` commands, `mflux-generate-ideogram4`, `mflux-capabilities` never completed). Commands are now discovered from the installed console scripts and, where a CLI exposes `build_parser()`, completions are generated from the CLI's real parser instead of a hand-copied recipe of it. (#578, #651)
- **`mflux-capabilities` publishes wire types, not Python converter names**: options validated by a named converter leaked the function name as their type (`--vae-tile-size` claimed type `vae_tile_size`, `--mlx-cache-limit-gb` claimed `positive_float`, every Path option claimed `PosixPath`). Named converters now map to what they yield: `int`, `float`, `path`, and `int-or-scale` for values that accept a pixel count, a `2x` factor or `auto`. The dump's type field is pinned to that closed vocabulary by a test. (#578, #652)
- **The metadata sidecar of an in-context result can reproduce the run**: `get_right_half()` dropped `negative_prompt`, `image_paths` and the redux fields when cloning metadata onto the cropped half, and the `--save-full-image` composite was saved without a sidecar even under `--metadata`. `redux_image_paths` was also serialized as a Python repr (`"['a.png', 'b.png']"`) instead of a list, so `--config-from-metadata` could never read it back. The unreachable restore branches for `controlnet_save_canny` and `image_outpaint_padding`, keys no released writer has ever emitted, are removed along with a stray debug `print()` in the outpaint parse block. (#578, #649)

## [0.19.0] - 2026-08-18

Expand Down
8 changes: 0 additions & 8 deletions src/mflux/cli/parser/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,6 @@ def parse_args(self) -> argparse.Namespace: # type: ignore
namespace.controlnet_image_path = prior_gen_metadata.get("controlnet_image_path", None)
if namespace.controlnet_strength == self.get_default("controlnet_strength") and (cnet_strength_from_metadata := prior_gen_metadata.get("controlnet_strength", None)):
namespace.controlnet_strength = cnet_strength_from_metadata
if not self._option_was_provided("--controlnet-save-canny", "--no-controlnet-save-canny") and (cnet_canny_from_metadata := prior_gen_metadata.get("controlnet_save_canny", None)) is not None:
namespace.controlnet_save_canny = cnet_canny_from_metadata


if self.supports_image_outpaint:
if namespace.image_outpaint_padding is None:
namespace.image_outpaint_padding = prior_gen_metadata.get("image_outpaint_padding", None)

if hasattr(namespace, "pid_decode") and not self._option_was_provided("--pid-decode", "--no-pid-decode"):
namespace.pid_decode = prior_gen_metadata.get("pid_decode", False)
Expand Down Expand Up @@ -569,7 +562,6 @@ def parse_args(self) -> argparse.Namespace: # type: ignore
if self.supports_image_outpaint and namespace.image_outpaint_padding is not None:
# parse and normalize any acceptable 1,2,3,4-tuple box value to 4-tuple
namespace.image_outpaint_padding = box_values.BoxValues.parse(namespace.image_outpaint_padding)
print(f"{namespace.image_outpaint_padding=}")

# Resolve lora paths from library if needed
if self.supports_lora and hasattr(namespace, "lora_paths") and namespace.lora_paths:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def main():
output_path = Path(args.output.format(seed=seed))
image.get_right_half().save(path=output_path, export_json_metadata=args.metadata)
if args.save_full_image:
image.save(path=output_path.with_stem(output_path.stem + "_full"))
image.save(path=output_path.with_stem(output_path.stem + "_full"), export_json_metadata=args.metadata)

except (StopImageGenerationException, PromptFileReadError) as exc:
print(exc)
Expand Down
2 changes: 1 addition & 1 deletion src/mflux/models/flux/cli/flux_generate_in_context_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def main():
output_path = Path(args.output.format(seed=seed))
image.get_right_half().save(path=output_path, export_json_metadata=args.metadata)
if args.save_full_image:
image.save(path=output_path.with_stem(output_path.stem + "_full"))
image.save(path=output_path.with_stem(output_path.stem + "_full"), export_json_metadata=args.metadata)

except (StopImageGenerationException, PromptFileReadError) as exc:
print(exc)
Expand Down
2 changes: 1 addition & 1 deletion src/mflux/models/flux/cli/flux_generate_in_context_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def main():
output_path = Path(args.output.format(seed=seed))
image.get_right_half().save(path=output_path, export_json_metadata=args.metadata)
if args.save_full_image:
image.save(path=output_path.with_stem(output_path.stem + "_full"))
image.save(path=output_path.with_stem(output_path.stem + "_full"), export_json_metadata=args.metadata)

except (StopImageGenerationException, PromptFileReadError) as exc:
print(exc)
Expand Down
6 changes: 5 additions & 1 deletion src/mflux/utils/generated_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,14 @@ def get_right_half(self) -> "GeneratedImage":
controlnet_image_path=self.controlnet_image_path,
controlnet_strength=self.controlnet_strength,
image_path=self.image_path,
image_paths=self.image_paths,
image_strength=self.image_strength,
masked_image_path=self.masked_image_path,
depth_image_path=self.depth_image_path,
redux_image_paths=self.redux_image_paths,
redux_image_strengths=self.redux_image_strengths,
concept_heatmap=self.concept_heatmap,
negative_prompt=self.negative_prompt,
init_metadata=self.init_metadata,
pid_decode=self.pid_decode,
pid_degrade_sigma=self.pid_degrade_sigma,
Expand Down Expand Up @@ -236,7 +240,7 @@ def _get_metadata(self) -> dict:
"controlnet_strength": round(self.controlnet_strength, 2) if self.controlnet_strength else None,
"masked_image_path": str(self.masked_image_path) if self.masked_image_path else None,
"depth_image_path": str(self.depth_image_path) if self.depth_image_path else None,
"redux_image_paths": str(self.redux_image_paths) if self.redux_image_paths else None,
"redux_image_paths": [str(p) for p in self.redux_image_paths] if self.redux_image_paths else None,
"redux_image_strengths": self._format_redux_strengths(),
"prompt": self.prompt,
"negative_prompt": self.negative_prompt if self.negative_prompt else None,
Expand Down
5 changes: 3 additions & 2 deletions tests/arg_parser/test_cli_argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,10 +643,11 @@ def test_image_outpaint_args(mflux_generate_parser, mflux_generate_minimal_argv,
args = mflux_generate_parser.parse_args()
assert args.image_outpaint_padding is None

# test metadata config accepted
# A sidecar key is ignored: no released writer ever emitted image_outpaint_padding,
# so the restore branch for it was deleted rather than kept for hand-edited files.
with patch('sys.argv', mflux_generate_minimal_argv + ['--config-from-metadata', metadata_file.as_posix()]): # fmt: off
args = mflux_generate_parser.parse_args()
assert args.image_outpaint_padding == BoxValues(10, 20, 30, 40)
assert args.image_outpaint_padding is None

# test outpaint padding override in 4-value format
with patch('sys.argv', mflux_generate_minimal_argv + ['--image-outpaint-padding', '5,15,25,35', '--config-from-metadata', metadata_file.as_posix()]): # fmt: off
Expand Down
67 changes: 67 additions & 0 deletions tests/metadata/test_generated_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,73 @@ def test_pid_flags_are_absent_from_metadata_without_pid_decode(tmp_path):
assert "pid_degrade_sigma" not in metadata


def test_get_right_half_keeps_every_metadata_attribute():
original = GeneratedImage(
image=Image.new("RGB", (16, 16), "white"),
model_config=ModelConfig.dev(),
seed=42,
prompt="test prompt",
steps=20,
guidance=3.5,
precision=mx.bfloat16,
quantization=8,
generation_time=1.23,
lora_paths=["style.safetensors"],
lora_scales=[0.8],
height=16,
width=16,
controlnet_image_path="control.png",
controlnet_strength=0.6,
image_path="init.png",
image_paths=["ref1.png", "ref2.png"],
image_strength=0.4,
masked_image_path="mask.png",
depth_image_path="depth.png",
redux_image_paths=["redux1.png", "redux2.png"],
redux_image_strengths=[0.5, 0.7],
concept_heatmap=None,
negative_prompt="blurry",
init_metadata={"source": "test"},
pid_decode=True,
pid_degrade_sigma=0.2,
)

half = original.get_right_half()

# The right half is the deliverable of the in-context commands, so its sidecar must
# reproduce the run. Compare every stored attribute except the cropped image itself.
original_attrs = {k: v for k, v in vars(original).items() if k != "image"}
half_attrs = {k: v for k, v in vars(half).items() if k != "image"}
assert half_attrs == original_attrs
assert half.image.size == (8, 16)


def test_redux_image_paths_export_as_a_list(tmp_path):
output_path = tmp_path / "redux_output.png"
generated_image = GeneratedImage(
image=Image.new("RGB", (16, 16), "white"),
model_config=ModelConfig.dev(),
seed=42,
prompt="test prompt",
steps=20,
guidance=3.5,
precision=mx.bfloat16,
quantization=8,
generation_time=1.23,
height=16,
width=16,
redux_image_paths=["redux1.png", "redux2.png"],
redux_image_strengths=[0.5, 0.7],
)

generated_image.save(path=output_path, overwrite=True, export_json_metadata=True)

metadata = json.loads(output_path.with_suffix(".metadata.json").read_text())
# Was str(list), i.e. a Python repr the restore side could never parse back.
assert metadata["redux_image_paths"] == ["redux1.png", "redux2.png"]
assert metadata["image_paths"] is None


def test_fibo_edit_save_keeps_prompt_json_and_exports_metadata_separately(tmp_path):
output_path = tmp_path / "fibo_edit_output.png"
prompt = json.dumps(
Expand Down