diff --git a/florence2_hires.py b/florence2_hires.py index 7527103..6e80b91 100644 --- a/florence2_hires.py +++ b/florence2_hires.py @@ -839,7 +839,7 @@ def run_florence2_step( def build_florence2_manifest_entry( step_result, item, *, session_name, pipeline_name, upscale_id, - upscaled_filename, current_index, hires_denoise + upscaled_filename, current_index, hires_denoise, base_dims=None ): """Build the manifest entry dict for a successful Florence2 hi-res-fix. @@ -876,17 +876,19 @@ def build_florence2_manifest_entry( "florence2_bbox": step_result.get("florence2_bbox"), "hires_denoise": hires_denoise, }) + if base_dims: + base.update(base_dims) return base -def build_florence2_no_detection_entry(step_result, item, *, sentinel_id, current_index): +def build_florence2_no_detection_entry(step_result, item, *, sentinel_id, current_index, base_dims=None): """Build a sentinel manifest entry for a Florence2 no-detection result. No new image file is written — the entry points back at the source filename so the dashboard can render a "no detection" badge alongside the original. """ text = step_result.get("florence2_text_input", "") - return { + entry = { "id": sentinel_id, "gen_index": current_index, "filename": item.get("filename", ""), @@ -897,3 +899,6 @@ def build_florence2_no_detection_entry(step_result, item, *, sentinel_id, curren "florence2_model": step_result.get("florence2_model", ""), "note": f"Florence2 found no '{text}' in image", } + if base_dims: + entry.update(base_dims) + return entry diff --git a/generation_orchestrator.py b/generation_orchestrator.py index ffcc7b3..2a4c308 100644 --- a/generation_orchestrator.py +++ b/generation_orchestrator.py @@ -633,6 +633,7 @@ def run_deferred_upscales( upscaled_meta["id"] = upscale_id upscaled_meta["filename"] = upscaled_filename upscaled_meta["upscaled"] = True + upscaled_meta["base_width"], upscaled_meta["base_height"] = pipe_w, pipe_h upscaled_meta["upscale_pipeline"] = pipeline_name if job.get("config_fp"): from .config_utils import config_fingerprint as _cfp @@ -2112,6 +2113,7 @@ def run_generation_loop( ) sentinel_meta.update({ "id": sentinel_id, + "base_width": w, "base_height": h, "base_fp": job_fp, "config_fp": config_fingerprint(conf, w, h, conf["seed"], pipeline_name), "upscaled": False, @@ -2156,6 +2158,7 @@ def run_generation_loop( "filename": upscaled_filename, "rejected": False, "upscaled": True, + "base_width": w, "base_height": h, "upscale_source": "inline", "upscale_pipeline": pipeline_name, "upscale_mode": "florence2_hires", @@ -2230,6 +2233,7 @@ def run_generation_loop( "width": up_w, "height": up_h, "duration": sv_duration, "upscaled": True, + "base_width": w, "base_height": h, "upscale_source": "inline", "upscale_pipeline": pipeline_name, "upscale_mode": "seedvr2", @@ -2395,6 +2399,7 @@ def run_generation_loop( ) upscaled_meta["id"] = upscale_id upscaled_meta["upscaled"] = True + upscaled_meta["base_width"], upscaled_meta["base_height"] = w, h upscaled_meta["upscale_pipeline"] = pipeline_name upscaled_meta["base_fp"] = job_fp upscaled_meta["config_fp"] = config_fingerprint(conf, w, h, conf["seed"], pipeline_name) diff --git a/manifest_utils.py b/manifest_utils.py index 9c91efb..56bd70d 100644 --- a/manifest_utils.py +++ b/manifest_utils.py @@ -3,6 +3,7 @@ Manifest File Management Handles saving and merging manifest.json with user modifications """ +import hashlib import json import os import re @@ -314,3 +315,14 @@ def append_items(manifest_path, items, remove_ids=(), session_name=None, default json.dump(data, f, indent=4) os.replace(tmp, manifest_path) return data + + +def base_dims_of(item): + """{"base_width","base_height"} of the generated image an upscale of `item` starts from ({} if unknown).""" + bw, bh = item.get("base_width"), item.get("base_height") + if bw and bh: + return {"base_width": bw, "base_height": bh} + if item.get("upscaled"): + return {} # legacy upscaled source: let normResolution estimate + w, h = item.get("width"), item.get("height") + return {"base_width": w, "base_height": h} if w and h else {} diff --git a/resources/logic_compare.js b/resources/logic_compare.js index dadc2ca..96c56c5 100644 --- a/resources/logic_compare.js +++ b/resources/logic_compare.js @@ -307,7 +307,7 @@ function cmpBuildPool() { var src = processedData || []; for (var i = 0; i < src.length; i++) { var d = src[i]; - if (d && d.file && !d.rejected) out.push(d); // a reject is a decision already made + if (d && d.file && !d.rejected && !d.florence2_no_detection) out.push(d); // a reject is a decision already made } return out; } diff --git a/resources/logic_ratings.js b/resources/logic_ratings.js index fd42774..c457221 100644 --- a/resources/logic_ratings.js +++ b/resources/logic_ratings.js @@ -20,7 +20,10 @@ var CMP_EXCLUDED_KEYS = new Set([ 'note', 'notes', 'positive', 'config_positive', 'negative', 'config_negative', 'seed', 'width', 'height', 'base_width', 'base_height', 'model', 'lora', 'lora_expanded', 'sampler', 'scheduler', 'cfg', 'steps', 'denoise', 'upscaled', 'source_file', - 'image_path', 'preview_path', 'video_path', 'hash', 'hires_denoise' + 'image_path', 'preview_path', 'video_path', 'hash', 'hires_denoise', + // per-image outcomes / bookkeeping, not settings + 'config_fp', 'base_fp', 'metrics', 'preview_of', 'source_relpath', + 'florence2_bbox', 'florence2_detection_count', 'florence2_no_detection' ]); var CMP_EXCLUDED_RE = /^upscale_|^hires_prompt_|_hash$|^worker|^_/; @@ -89,6 +92,7 @@ function normResolution(d) { } else if (!d.upscaled) { k = d.width + 'x' + d.height; } else { + // legacy manifests (before base_width/base_height): estimate from upscale_ratio var r = cmpProduct(d.upscale_ratio); if (r > 0 && d.width > 0 && d.height > 0) { k = (Math.round(d.width / r / 8) * 8) + 'x' + (Math.round(d.height / r / 8) * 8); diff --git a/tests/conftest.py b/tests/conftest.py index bbe6367..3de3111 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,6 +108,7 @@ _GEN_ORCH_DEPS = { "load_existing_manifest": lambda *a, **kw: {}, "save_manifest": lambda *a, **kw: None, "preserve_disk_comparisons": lambda *a, **kw: None, + "base_dims_of": lambda item: {}, "update_manifest_comparisons": lambda *a, **kw: {"comparisons": [], "added": 0, "removed": 0, "total": 0}, }, "model_loader": { diff --git a/tests/js/ratings.test.js b/tests/js/ratings.test.js index de84855..6fbdb55 100644 --- a/tests/js/ratings.test.js +++ b/tests/js/ratings.test.js @@ -270,3 +270,18 @@ test('mergeComparisons dedupes by ts:a:b', () => { const m = R.mergeComparisons([{ a: 1, b: 2, ts: 5 }], [{ a: 1, b: 2, ts: 5 }, { a: 1, b: 2, ts: 6 }, { a: 2, b: 1, ts: 5 }]); assert.strictEqual(m.length, 3); }); + +// ---- wave 2 polish ---- +test('exact base dims win over a conflicting ratio guess; also on a non-upscaled item', () => { + const d = { width: 1248, height: 1824, upscaled: true, upscale_ratio: 1.5, base_width: 832, base_height: 1216 }; + assert.strictEqual(R.normalizeAxis('resolution', d, {}).key, '832x1216'); + assert.strictEqual(R.normalizeAxis('resolution', { width: 100, height: 100, base_width: 64, base_height: 96 }, {}).key, '64x96'); +}); +test('per-image fields never make two items different settings (F2)', () => { + const a = item({ config_fp: 'a', base_fp: 'x', metrics: { s: 1 }, preview_of: 5, source_relpath: 'p/a.png' }); + const b = item({ cfg: 5, config_fp: 'b', base_fp: 'y', metrics: { s: 2 }, preview_of: 6, source_relpath: 'p/b.png', + florence2_bbox: [1, 2, 3, 4], florence2_detection_count: 2 }); + const df = R.axisDiff(a, b, { withCtxKeys: true }); + assert.strictEqual(df.ctxEqual, true, JSON.stringify(df.ctxKeys)); + assert.deepStrictEqual(df.axes, ['cfg']); +}); diff --git a/tests/test_florence2_manifest_entry.py b/tests/test_florence2_manifest_entry.py index a572b2c..b4c79eb 100644 --- a/tests/test_florence2_manifest_entry.py +++ b/tests/test_florence2_manifest_entry.py @@ -143,3 +143,23 @@ def test_no_detection_entry_does_not_carry_upscale_metadata(): # They don't claim to be an upscale output assert entry["upscaled"] is False assert "upscale_mode" not in entry + + +def test_base_dims_kwarg_lands_in_both_builders(): + from florence2_hires import build_florence2_manifest_entry, build_florence2_no_detection_entry + + step = {"status": "ok", "image_width": 1536, "image_height": 1536, "florence2_text_input": "face"} + kw = dict(session_name="s", pipeline_name="P", upscale_id=1, upscaled_filename="f.webp", + current_index=0, hires_denoise=0.5) + item = {"id": 9, "file": "/view?x", "filename": "x.webp"} + plain = build_florence2_manifest_entry(step, item, **kw) + assert "base_width" not in plain and "base_height" not in plain + dims = {"base_width": 1024, "base_height": 1024} + withd = build_florence2_manifest_entry(step, item, base_dims=dims, **kw) + assert withd["base_width"] == 1024 and withd["base_height"] == 1024 + assert {k: v for k, v in withd.items() if not k.startswith("base_")} == plain + + s_plain = build_florence2_no_detection_entry(step, item, sentinel_id=3, current_index=0) + assert "base_width" not in s_plain + s_dims = build_florence2_no_detection_entry(step, item, sentinel_id=3, current_index=0, base_dims=dims) + assert s_dims["base_width"] == 1024 and s_dims["florence2_no_detection"] is True diff --git a/tests/test_upscaled_base_dims.py b/tests/test_upscaled_base_dims.py new file mode 100644 index 0000000..ebd54b6 --- /dev/null +++ b/tests/test_upscaled_base_dims.py @@ -0,0 +1,55 @@ +"""Source guard: every place that writes an upscaled manifest item also records base_width/base_height.""" +import os +import re + +_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_WRITE = re.compile(r"""("upscaled": True|\["upscaled"\] = True)""") +_SENTINEL = re.compile(r"\n\s*(?:sentinel_meta\.update\(|build_florence2_no_detection_entry\()") +_BASE = ("base_width", "base_dims_of", "base_dims=") + + +def _read(name): + with open(os.path.join(_ROOT, name), encoding="utf-8") as f: + return f.read() + + +def _blocks(text): + """Text from each upscaled-True write up to the manifest insert/append that follows it.""" + out = [] + for m in _WRITE.finditer(text): + tail = text[m.end():] + end = re.search(r"(items\"\]\.(insert|append)\()", tail) + out.append((m.start(), text[max(0, m.start() - 1500): m.end() + (end.end() if end else 1500)])) + return out + + +def test_every_upscaled_write_records_base_dims(): + total = 0 + for name in ("generation_orchestrator.py", "upscale_runner.py"): + text = _read(name) + for pos, block in _blocks(text): + total += 1 + line = text.count("\n", 0, pos) + 1 + assert any(tok in block for tok in _BASE), f"{name}:{line} writes an upscaled item without base dims" + assert total >= 6 # 1 deferred + 3 inline + 2 dashboard (sanity: the regex still finds them) + + +def test_sentinels_record_base_dims(): + for name in ("generation_orchestrator.py", "upscale_runner.py"): + text = _read(name) + for m in _SENTINEL.finditer(text): + assert any(tok in text[m.end(): m.end() + 900] for tok in _BASE), f"{name}: sentinel without base dims" + + +def test_base_dims_of_cases(): + import importlib.util + spec = importlib.util.spec_from_file_location("_mu_real", os.path.join(_ROOT, "manifest_utils.py")) + mu = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mu) + f = mu.base_dims_of + assert f({"width": 832, "height": 1216}) == {"base_width": 832, "base_height": 1216} + assert f({"width": 1248, "height": 1824, "upscaled": True, "base_width": 832, "base_height": 1216}) == \ + {"base_width": 832, "base_height": 1216} + assert f({"width": 1248, "height": 1824, "upscaled": True}) == {} + assert f({}) == {} + assert f({"width": 0, "height": 5}) == {} diff --git a/upscale_runner.py b/upscale_runner.py index cfc94af..06b4259 100644 --- a/upscale_runner.py +++ b/upscale_runner.py @@ -18,7 +18,7 @@ from PIL import Image as PILImage from .model_loader import load_checkpoint, load_vae_by_name from .batch_encoding import encode_prompt_with_combinators, encode_pair_cached from .image_generation import upscale_image, decode_latent_with_vae, create_image_metadata -from .manifest_utils import save_manifest +from .manifest_utils import save_manifest, base_dims_of from . import image_metrics import comfy.model_management as mm @@ -346,6 +346,7 @@ def _run_upscale_thread(job, target_items, upscale_config, meta, manifest_data, "width": up_w, "height": up_h, "duration": round(sv_duration, 2), "upscaled": True, + **base_dims_of(item), "upscale_source": "dashboard", "upscale_pipeline": pipeline_name, "upscale_mode": "seedvr2", @@ -420,6 +421,7 @@ def _run_upscale_thread(job, target_items, upscale_config, meta, manifest_data, step_result, item, sentinel_id=sentinel_id, current_index=len(manifest_data["items"]), + base_dims=base_dims_of(item), ) manifest_data["items"].insert(0, entry) upscale_combo_idx += 1 @@ -448,6 +450,7 @@ def _run_upscale_thread(job, target_items, upscale_config, meta, manifest_data, upscaled_filename=upscaled_filename, current_index=len(manifest_data["items"]), hires_denoise=f2_config["hires_denoise"], + base_dims=base_dims_of(item), ) image_metrics.finalize(entry, result_pil, spec=item_metrics_spec) manifest_data["items"].insert(0, entry) @@ -561,6 +564,7 @@ def _run_upscale_thread(job, target_items, upscale_config, meta, manifest_data, "height": up_h, "duration": round(upscale_duration, 2), "upscaled": True, + **base_dims_of(item), "upscale_source": "dashboard", "upscale_pipeline": pipeline_name, "upscale_mode": mode,