Support re-import for recipes without a source URL

Recipes imported by drag & drop / file-picker record no source_path and
were rejected by re-import. Fall back to the recipe's own saved image,
which still carries the original embedded generation metadata.

Re-import now re-parses that original metadata instead of the appended
recipe JSON block, so parser upgrades produce fresh results. The
already-optimized preview image is kept verbatim: only its WebP EXIF
chunk is rewritten in place to replace the recipe metadata block, and
the recipe JSON is rewritten with the new analysis plus carried-over
user edits.
This commit is contained in:
Will Miao
2026-08-31 10:01:18 +08:00
parent 2a3c632dc5
commit 39e7c1376c
9 changed files with 537 additions and 41 deletions
+63
View File
@@ -65,6 +65,69 @@ def test_append_recipe_metadata_includes_checkpoint(monkeypatch, tmp_path):
assert payload["base_model"] == "Illustrious"
def test_append_recipe_metadata_pixel_preserving_webp(tmp_path):
"""pixel_preserving=True must rewrite only the EXIF chunk of a WebP,
leaving the pixel chunks byte-identical and the old block replaced."""
img = Image.new("RGB", (64, 48), (120, 30, 200))
original_params = (
"masterpiece, best quality\n"
"Negative prompt: lowres\n"
"Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1, "
"Size: 512x768, Model hash: abc123, Model: foo_v1, Clip skip: 2\n"
' Recipe metadata: {"title": "Old", "loras": []}'
)
exif = piexif.dump(
{
"0th": {},
"Exif": {
piexif.ExifIFD.UserComment: b"UNICODE\x00"
+ original_params.encode("utf-16be")
},
}
)
image_path = tmp_path / "recipe.webp"
img.save(str(image_path), format="WEBP", exif=exif, quality=85)
with open(image_path, "rb") as fh:
before = fh.read()
new_recipe = {
"title": "New",
"base_model": "SDXL",
"loras": [],
"gen_params": {"steps": 25},
}
ExifUtils.append_recipe_metadata(
str(image_path), new_recipe, pixel_preserving=True
)
with open(image_path, "rb") as fh:
after = fh.read()
def chunks(data: bytes) -> Dict[bytes, bytes]:
pos, result = 12, {}
while pos + 8 <= len(data):
fourcc = data[pos : pos + 4]
size = int.from_bytes(data[pos + 4 : pos + 8], "little")
result[fourcc] = data[pos + 8 : pos + 8 + size]
pos += 8 + size + (size % 2)
return result
before_chunks = chunks(before)
after_chunks = chunks(after)
for fourcc, payload in before_chunks.items():
if fourcc == b"EXIF":
assert after_chunks[fourcc] != payload, "EXIF must be replaced"
else:
assert after_chunks[fourcc] == payload, f"{fourcc} was re-encoded"
# The appended block is updated; the original parameters stay in front.
metadata = ExifUtils.extract_image_metadata(str(image_path))
assert "Steps: 20" in metadata
assert 'Recipe metadata: {"title": "New"' in metadata
assert '"title": "Old"' not in metadata
def test_optimize_image_preserves_workflow_when_converting_png_to_webp(tmp_path):
image_path = tmp_path / "source.png"
png_info = PngImagePlugin.PngInfo()