diff --git a/tools/re-capture/screen_id.py b/tools/re-capture/screen_id.py index 5a09dad3..8de3e198 100755 --- a/tools/re-capture/screen_id.py +++ b/tools/re-capture/screen_id.py @@ -35,7 +35,34 @@ import sys W, H = 320, 180 +def frame_size(path): + """(w, h) of the capture, or None if it cannot be read.""" + out = subprocess.run(["identify", "-format", "%w %h", path], + capture_output=True, text=True).stdout.split() + try: + return int(out[0]), int(out[1]) + except (IndexError, ValueError): + return None + + +# A capture that is not a plausible game frame must be REJECTED, not classified. +# MEASURED 2026-08-26: a guard shot came back 10x710 -- a sliver -- and every +# statistic below is an area fraction, so the sliver classified cleanly as +# `menu`. The guard passed, the fixed key sequence was issued anyway, and the run +# loaded a TUTORIAL instead of the save's Stage 02. +# +# This is the SECOND time this has been paid for. `bin/screenshot`'s own header +# records the first (2026-08-18): a second window of class "xenia_canary" meant +# grabs came back as slivers and "a whole session's screen ids were noise". That +# fix hardened the CAPTURE side only, so the same failure still reaches the +# oracles by any other path. Reject it here too, where the answer is consumed. +MIN_W, MIN_H = 640, 360 + + def features(path): + size = frame_size(path) + if size is None or size[0] < MIN_W or size[1] < MIN_H: + return None raw = subprocess.run( ["convert", path, "-alpha", "off", "-resize", f"{W}x{H}!", "-depth", "8", "rgb:-"], capture_output=True).stdout @@ -57,7 +84,7 @@ def features(path): def classify(f): if f is None: - return "none" + return "none" # unreadable OR not a plausible frame -- never a screen # Flight first: the HUD paints far more green than any menu. # (READY ROOM is checked before the menu rule because it is also dark-ish # and strongly blue, and would otherwise be swallowed by it.)