diff --git a/image_generation.py b/image_generation.py index 8360bf0..cf25332 100644 --- a/image_generation.py +++ b/image_generation.py @@ -1123,6 +1123,8 @@ def flush_batch_with_vae(pending_batch, vae, img_dir, existing_data, session_nam local_item["favorited"] = disk_item["favorited"] if "rejected" in disk_item: local_item["rejected"] = disk_item["rejected"] + if "metrics" in disk_item and "metrics" not in local_item: + local_item["metrics"] = disk_item["metrics"] # computed later for an existing session except Exception as e: print(f"[GridTester] ⚠️ Error syncing with disk manifest: {e}") diff --git a/manifest_utils.py b/manifest_utils.py index 4121c67..5f8267a 100644 --- a/manifest_utils.py +++ b/manifest_utils.py @@ -185,6 +185,10 @@ def merge_manifest_user_changes(manifest_path, existing_data): if current_item.get("note"): item["note"] = current_item["note"] notes_preserved += 1 + # Per-image metrics computed for an existing session (metrics_runner) live only on + # disk; a generation run's in-memory copy must not wipe them on its next save. + if "metrics" in current_item and "metrics" not in item: + item["metrics"] = current_item["metrics"] # else: This is a new item, no merge needed # Log merge results if any user changes were preserved diff --git a/remote_vae.py b/remote_vae.py index 216c3dc..1c788df 100644 --- a/remote_vae.py +++ b/remote_vae.py @@ -262,6 +262,8 @@ class RemoteVAEDecodeWorker: local_item["favorited"] = disk_item["favorited"] if "rejected" in disk_item: local_item["rejected"] = disk_item["rejected"] + if "metrics" in disk_item and "metrics" not in local_item: + local_item["metrics"] = disk_item["metrics"] # computed later for an existing session except Exception as e: print(f"[GridTester] ⚠️ Error syncing with disk manifest: {e}") diff --git a/tests/test_metrics_preserved_on_save.py b/tests/test_metrics_preserved_on_save.py new file mode 100644 index 0000000..e374240 --- /dev/null +++ b/tests/test_metrics_preserved_on_save.py @@ -0,0 +1,55 @@ +"""A generation run's manifest save must not wipe per-image metrics that were computed +for an existing session (metrics_runner writes them straight to disk while the run's +in-memory copy of those items has no `metrics`). Disk wins for an item's metrics when +the in-memory item has none; freshly computed in-memory metrics are kept. + +Loads the real manifest_utils.py via importlib (tests/conftest.py registers a stub +under the package name), like test_comparisons_manifest.py does. +""" +import importlib.util +import json +import os + +_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_spec = importlib.util.spec_from_file_location("manifest_utils_real_metrics", os.path.join(_ROOT, "manifest_utils.py")) +mu = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(mu) + + +def _item(i, **extra): + d = {"id": i, "file": f"/view?filename=img_{i}.webp", "favorited": False, "rejected": False, "note": ""} + d.update(extra) + return d + + +def test_disk_metrics_survive_a_stale_in_memory_save(tmp_path): + path = str(tmp_path / "manifest.json") + disk = {"session_name": "s", "meta": {}, "items": [ + _item(2, metrics={"sharpness": 12.5, "entropy": 7.1}), + _item(1, metrics={"sharpness": 3.0}), + ]} + with open(path, "w", encoding="utf-8") as f: + json.dump(disk, f) + + # the generation run's copy predates the metrics job: same items, no metrics, plus a new image + stale = {"session_name": "s", "meta": {}, "items": [_item(3), _item(2), _item(1)]} + mu.save_manifest(path, stale) + + with open(path, encoding="utf-8") as f: + after = json.load(f) + by_id = {it["id"]: it for it in after["items"]} + assert set(by_id) == {1, 2, 3}, "the new image is kept" + assert by_id[2]["metrics"] == {"sharpness": 12.5, "entropy": 7.1} + assert by_id[1]["metrics"] == {"sharpness": 3.0} + assert "metrics" not in by_id[3] + + +def test_fresh_in_memory_metrics_are_not_overwritten_by_disk(tmp_path): + path = str(tmp_path / "manifest.json") + with open(path, "w", encoding="utf-8") as f: + json.dump({"session_name": "s", "meta": {}, "items": [_item(1, metrics={"sharpness": 1.0})]}, f) + fresh = {"session_name": "s", "meta": {}, "items": [_item(1, metrics={"sharpness": 99.0})]} + mu.save_manifest(path, fresh) + with open(path, encoding="utf-8") as f: + after = json.load(f) + assert after["items"][0]["metrics"] == {"sharpness": 99.0}