#!/usr/bin/env python3 """Capture the JAPANESE title (`GP_TITLE` build 7) at rest. Asked for by the port agent: its `title_jp` row drifted and it can say the two renderers moved apart but not which moved, because **no capture of the JP title exists in this corpus**. `MISSION.md` has carried this as "needs one more run" — the locale route is `set_console_language.py`, and the reason the earlier runs failed (Ⓐ needs a signed-in profile) is now known. "At rest" is **demonstrated, not assumed**: after the plate pulse says the title has settled, five frames are taken ~1.5 s apart and the port's own region of interest — 350×396 at (405, 74) in design space — is compared across them. If the logo stack is still moving, the frames will say so. ⚠️ The frames are display-space 1280×720 with the game surface at y=45 (`tbm-submenu-reached.txt`), so the design-space box is offset by that. jp_title_capture.py OUTDIR [wait_s] """ import subprocess import sys import time import numpy as np from PIL import Image OUT = sys.argv[1] WAIT = float(sys.argv[2]) if len(sys.argv) > 2 else 480 W, H = 1280, 720 NEED, CEIL, HOLD = 500, 2500, 12 SURF_Y = 45 ROI = (405, 74, 350, 396) # design-space x, y, w, h — the port's drifting block def _open(): return subprocess.Popen( ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", "-video_size", f"{W}x{H}", "-i", ":98", "-r", "4", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE, bufsize=W * H * 3 * 2) def glyph(a): r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2] return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum()) T0 = time.time() p, n, seg, streak = _open(), W * H * 3, time.time(), 0 frames = [] while True: el = time.time() - T0 if el > WAIT: print("TIMEOUT — the title never settled", flush=True); break if time.time() - seg > 30: p.kill(); p = _open(); seg = time.time() buf = p.stdout.read(n) if len(buf) < n: p.kill(); p = _open(); seg = time.time(); continue a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(np.uint8) c = glyph(a.astype(int)) streak = streak + 1 if NEED <= c <= CEIL else 0 if streak >= HOLD: print(f"[{el:7.1f}s] TITLE SETTLED (plate pulse, glyph {c}) — taking 5 frames", flush=True) for k in range(5): t0 = time.time() while time.time() - t0 < 1.5: b2 = p.stdout.read(n) if len(b2) < n: p.kill(); p = _open(); break a = np.frombuffer(b2, np.uint8).reshape(H, W, 3).astype(np.uint8) Image.fromarray(a).save(f"{OUT}/jp-title-{k}.png") frames.append(a.astype(int)) print(f" frame {k}: glyph {glyph(a.astype(int))}", flush=True) break p.kill() if len(frames) == 5: x, y, w, h = ROI rois = [f[SURF_Y + y:SURF_Y + y + h, x:x + w] for f in frames] print("\nROI stability — the port's 350x396 block at (405,74), design space:") for i in range(1, 5): d = np.abs(rois[i] - rois[0]) print(f" frame {i} vs 0: max |Δ| {d.max():3d}, pixels differing >8: " f"{int((d.max(axis=2) > 8).sum()):6d} / {d.shape[0]*d.shape[1]}", flush=True) full = [np.abs(frames[i] - frames[0]).max(axis=2) for i in range(1, 5)] print(" whole frame, for contrast (the plate pulses, so this SHOULD move):") for i, d in enumerate(full, 1): print(f" frame {i} vs 0: pixels differing >8: {int((d > 8).sum()):7d}", flush=True)