fix(manifest): keep per-item metrics written to disk when a generation run saves

metrics_runner computes metrics for an existing session straight to disk; a
generation run's in-memory copy of those items had no metrics and would have
wiped them on its next save. Disk wins for an item's metrics when the memory
item has none (merge_manifest_user_changes, flush_batch_with_vae's disk sync,
the remote VAE worker's disk sync). Fresh in-memory metrics are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jason Hoku
2026-11-04 00:17:52 -08:00
co-authored by Claude Fable 5.1
parent add7fbb2cb
commit e1936bfdad
4 changed files with 63 additions and 0 deletions
+2
View File
@@ -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}")
+4
View File
@@ -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
+2
View File
@@ -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}")
+55
View File
@@ -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}