From 098809d2fe721dd16668a3b596d65d35a531951b Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 5 Jul 2026 01:49:19 +0000 Subject: [PATCH] Reject sd-cli batch runs and clear stale output targets before a run --- studio/backend/core/inference/sd_cpp_args.py | 7 ++++++- studio/backend/core/inference/sd_cpp_engine.py | 3 +++ studio/backend/tests/test_sd_cpp_args.py | 12 ++++++++++-- studio/backend/tests/test_sd_cpp_engine.py | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/studio/backend/core/inference/sd_cpp_args.py b/studio/backend/core/inference/sd_cpp_args.py index c370777bab..09e3faa778 100644 --- a/studio/backend/core/inference/sd_cpp_args.py +++ b/studio/backend/core/inference/sd_cpp_args.py @@ -269,7 +269,12 @@ def build_sd_cpp_command( if params.seed is not None: cmd += ["--seed", str(int(params.seed))] if params.batch_count and params.batch_count != 1: - cmd += ["--batch-count", str(int(params.batch_count))] + # sd-cli names the extra batch images itself (output_2.png, ...) and the runner + # collects only the literal --output path, so a CLI batch would silently drop + # every image after the first. Batches go through the sdcpp server API instead. + raise ValueError( + "sd-cli runs are single-image; use the sdcpp server API for batch generation." + ) cmd += ["--output", output_path] if threads is not None: diff --git a/studio/backend/core/inference/sd_cpp_engine.py b/studio/backend/core/inference/sd_cpp_engine.py index 52e4d6d693..c6582b0cb8 100644 --- a/studio/backend/core/inference/sd_cpp_engine.py +++ b/studio/backend/core/inference/sd_cpp_engine.py @@ -364,6 +364,9 @@ class SdCppEngine: def _prepare_out(output_path: str) -> Path: out = Path(output_path) out.parent.mkdir(parents = True, exist_ok = True) + # Drop a stale file at the target so the post-run is_file() check proves THIS + # run produced the image, not a leftover from an earlier run at the same path. + out.unlink(missing_ok = True) return out def _run( diff --git a/studio/backend/tests/test_sd_cpp_args.py b/studio/backend/tests/test_sd_cpp_args.py index 4157d0e01f..fdc5aec863 100644 --- a/studio/backend/tests/test_sd_cpp_args.py +++ b/studio/backend/tests/test_sd_cpp_args.py @@ -166,10 +166,18 @@ def test_build_appends_offload_and_extra_args_last(): def test_build_negative_prompt_and_batch(): files = SdCppModelFiles(diffusion_model = "/m/z.gguf") - params = SdCppGenParams(prompt = "x", negative_prompt = "blurry", batch_count = 3) + params = SdCppGenParams(prompt = "x", negative_prompt = "blurry") cmd = build_sd_cpp_command("/bin/sd-cli", files, params, output_path = "/o.png") assert _pair(cmd, "--negative-prompt") == "blurry" - assert _pair(cmd, "--batch-count") == "3" + # A CLI batch would silently drop every image after the first (the runner only + # collects the literal --output path), so the builder rejects it outright. + with pytest.raises(ValueError, match = "single-image"): + build_sd_cpp_command( + "/bin/sd-cli", + files, + SdCppGenParams(prompt = "x", batch_count = 3), + output_path = "/o.png", + ) def test_build_omits_unset_optional_params(): diff --git a/studio/backend/tests/test_sd_cpp_engine.py b/studio/backend/tests/test_sd_cpp_engine.py index daaf5223a8..8fee117b3a 100644 --- a/studio/backend/tests/test_sd_cpp_engine.py +++ b/studio/backend/tests/test_sd_cpp_engine.py @@ -323,6 +323,22 @@ def test_generate_raises_when_no_output_despite_success(tmp_path, monkeypatch): ) +def test_generate_does_not_return_stale_preexisting_output(tmp_path, monkeypatch): + # A leftover file at the target path must not satisfy the post-run output check + # when the run itself produced nothing: the target is cleared before the run. + e = _engine(tmp_path) + out = tmp_path / "img.png" + out.write_bytes(b"stale") + _patch_popen(monkeypatch, lines = ["ok"], returncode = 0, out_file = out, write = False) + with pytest.raises(RuntimeError, match = "no image"): + e.generate( + SdCppModelFiles(diffusion_model = "/m/z.gguf"), + SdCppGenParams(prompt = "x"), + output_path = str(out), + ) + assert not out.exists() + + def test_generate_raises_when_binary_missing(): e = SdCppEngine(binary = None) with pytest.raises(RuntimeError, match = "not found"):