tools: reject degenerate captures in screen_id instead of classifying them

Every statistic in screen_id is an AREA FRACTION, so a capture that is not a game
frame still produces clean numbers.  Measured 2026-08-26: a guard shot came back
10x710 -- a sliver -- and classified as `menu` with green=0.0000, white=0.0157.
The guard passed, the fixed key sequence went out anyway, and the run loaded a
TUTORIAL instead of the save's Stage 02.

This is the second time this failure 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 reached the
oracles by any other path.  Reject it at the point the answer is consumed too:
features() now returns None below 640x360 and classify() reports `none`.

Verified: the 10x710 sliver -> `none`; readyroom, flight and the briefing capture
all still classify as before.
This commit is contained in:
Sylpheed RE agent
2026-08-26 22:55:58 +00:00
parent 67001f04fb
commit 30e53f599c
2 changed files with 28 additions and 1 deletions

View File

@@ -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.)