re: the main menu has no idle self-return -- and four of my own durations were a backlog

Two results and one retraction, all from the same session.

REFUTED: 'an ~8-10 s idle returns to the title' does not apply to the main menu.
Held untouched it stayed put for >= 60 s, correlation never leaving
0.9245-0.9249. That timer is real but belongs to the TITLE. It was the only
reason 'B leaves the main menu' was classed as authored, so Q5's B rule is
upgraded to measured-ordering: B is delivered (canary logs vk=5801) and is the
only input in >= 100 s before the return.

The PRESS (A) plate: the boot title presents build 4 WITHOUT the plate first --
green-glyph 154, against 159 on the committed no-plate capture and 753/977/1493
on plate titles -- and the plate arrives after. That is the port's third option.

RETRACTED: four durations taken the same day. classify_array costs 1503 ms per
frame; running it per frame against an 8 fps x11grab drained the pipe at
0.64 fps, so every classified frame was stale and increasingly so. It
manufactured 'plate 24.66 s after the title art', 'B->title 15.58 s', 'B->title
25.60 s' and 'A->menu 20.26 s'. The tell: a transition, a press and a fade do
not share a duration, and the two B figures GREW across a longer run.

A backlog preserves ordering and destroys durations, which is why the sequence
results above stand and every timing does not. The ring's period is unaffected
and that was checked, not assumed -- ring_period ran at 15.03 fps against a
requested 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP
This commit is contained in:
sylph-decoder
2026-08-29 11:50:46 +00:00
parent 724e06b134
commit 7a4a74f8d7
7 changed files with 427 additions and 8 deletions

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Does (B) leave the main menu -- and does the menu self-return to the title?
HANDOFF downgraded "(B) on the main menu returns to the title" to authored,
because the corpus also carries "an ~8-10 s idle returns to the title" and one
unrecorded observation cannot separate the two causes. This separates them by
ordering: hold the menu UNTOUCHED for an idle window several times longer than
the claimed 8-10 s and timestamp what happens, THEN press (B) and timestamp
again. If the idle window passes with the menu still up, the idle cause is
gone and the (B) observation is unambiguous.
Screen identity comes from screen_match.py, whose control includes the movie
frames that broke the statistics-based oracle.
Usage: menu_b_probe.py IDLE_SECONDS AFTER_SECONDS
"""
import os, subprocess, sys, time
import numpy as np
from PIL import Image
SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SD)
from screen_match import classify_array
W, H = 1280, 720
IDLE = float(sys.argv[1]) if len(sys.argv) > 1 else 60.0
AFTER = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
OUT = "/sylph-home/re/ringcap"
def stream():
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 main():
p = stream(); n = W * H * 3
t0 = time.time(); seg = t0; last = None; prev = None
phase = "IDLE"; pressed_at = None
log = []
while True:
el = time.time() - t0
if phase == "IDLE" and el >= IDLE:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "B", "0.3"], check=False)
pressed_at = time.time() - t0
print(f"t={pressed_at:6.2f}s >>> (B) PRESSED", flush=True)
phase = "AFTER"
if phase == "AFTER" and el >= IDLE + AFTER:
break
if time.time() - seg > 25:
p.kill(); p = stream(); seg = time.time()
b = p.stdout.read(n)
if len(b) < n:
p.kill(); p = stream(); seg = time.time(); continue
a = np.frombuffer(b, np.uint8).reshape(H, W, 3)
last = a
c, sc = classify_array(a)
log.append((el, c, sc["title"], sc["menu"]))
if c != prev:
print(f"t={el:6.2f}s screen={c:<6} title={sc['title']:+.3f} "
f"menu={sc['menu']:+.3f}", flush=True)
Image.fromarray(a).save(f"{OUT}/b-{el:06.2f}-{c}.png")
prev = c
p.kill()
with open(f"{OUT}/menu-b-trace.tsv", "w") as f:
f.write("t_s\tscreen\tcorr_title\tcorr_menu\n")
for r in log:
f.write(f"{r[0]:.3f}\t{r[1]}\t{r[2]:.4f}\t{r[3]:.4f}\n")
idle = [r for r in log if r[0] < IDLE]
aft = [r for r in log if pressed_at and r[0] > pressed_at + 1.0]
print(f"\nIDLE phase : {len(idle)} samples over {IDLE:.0f}s, "
f"screens seen = {sorted(set(r[1] for r in idle))}")
print(f"AFTER (B) : {len(aft)} samples, "
f"screens seen = {sorted(set(r[1] for r in aft))}")
first_title = next((r[0] for r in aft if r[1] == "title"), None)
if first_title:
print(f" first 'title' at t={first_title:.2f}s = "
f"{first_title - pressed_at:.2f}s after the (B) press")
print(f"trace: {OUT}/menu-b-trace.tsv")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""One boot, two answers: when the PRESS (A) plate appears, and what (B) does.
Q(plate): the port's boot ends on GP_TITLE build 4, which carries no plate, and
(A) is the only way off it -- so it ships a screen that needs a press and does
not say so. Builds 2/3 are the plate. The open half is the SEQUENCE: build 4
alone, build 4 with the plate composited from the start, or build 4 and THEN
the plate after a delay. This logs the title-art correlation and the green-(A)
glyph count on EVERY frame from before the title appears, so the two crossings
are read off one trace rather than inferred.
Q(B): whether (B) leaves the main menu, timed against the idle alternative.
Controls, both pre-run on committed captures:
* screen identity -- screen_match.py, 8/8 including the movie frames that
broke the statistics oracle;
* the plate -- green-(A) glyph count: 753/977/1493 px on plate titles, 159 on
`live-title-build4-no-plate.png`, 327 on the main menu. Threshold 400.
Usage: title_plate_and_b_probe.py OUTDIR
"""
import os, subprocess, sys, time
import numpy as np
from PIL import Image
SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SD)
from screen_match import classify_array
W, H = 1280, 720
PLATE = 400
OUT = sys.argv[1] if len(sys.argv) > 1 else "/sylph-home/re/platecap"
os.makedirs(OUT, exist_ok=True)
def stream():
return subprocess.Popen(
["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0",
"-video_size", f"{W}x{H}", "-i", ":98", "-r", "8",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
stdout=subprocess.PIPE, bufsize=W * H * 3 * 2)
def glyph(a):
r, g, b = a[:, :, 0].astype(int), a[:, :, 1].astype(int), a[:, :, 2].astype(int)
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def main():
p = stream(); n = W * H * 3
t0 = time.time(); seg = t0; prev = None
skipped = False; stage = 0; marks = {}
rows = []
while time.time() - t0 < 420:
if time.time() - seg > 25:
p.kill(); p = stream(); seg = time.time()
b = p.stdout.read(n)
if len(b) < n:
p.kill(); p = stream(); seg = time.time(); continue
a = np.frombuffer(b, np.uint8).reshape(H, W, 3)
el = time.time() - t0
c, sc = classify_array(a)
gl = glyph(a)
rows.append((el, c, sc["title"], sc["menu"], gl))
if c != prev:
print(f"t={el:7.2f}s screen={c:<6} title={sc['title']:+.3f} "
f"menu={sc['menu']:+.3f} glyph={gl}", flush=True)
prev = c
if not skipped and el > 45:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "A", "0.3"], check=False)
skipped = True
print(f"t={el:7.2f}s one (A) to skip the intro movie", flush=True)
elif stage == 0 and c == "title":
marks["title_art"] = el; stage = 1
Image.fromarray(a).save(f"{OUT}/title-first-{el:07.2f}.png")
print(f"t={el:7.2f}s TITLE ART (glyph={gl}) — watching for the plate",
flush=True)
elif stage == 1 and gl >= PLATE:
marks["plate"] = el; stage = 2
Image.fromarray(a).save(f"{OUT}/title-plate-{el:07.2f}.png")
print(f"t={el:7.2f}s PLATE (glyph={gl}) — "
f"{el-marks['title_art']:.2f}s after the title art", flush=True)
elif stage == 2 and el > marks["plate"] + 6:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "A", "0.3"], check=False)
marks["A"] = el; stage = 3
print(f"t={el:7.2f}s >>> (A) on the title", flush=True)
elif stage == 3 and c == "menu":
marks["menu"] = el; stage = 4
print(f"t={el:7.2f}s MENU — idling 25 s before (B)", flush=True)
elif stage == 4 and el > marks["menu"] + 25:
subprocess.run(["python3", f"{SD}/pad.py", "tap", "B", "0.3"], check=False)
marks["B"] = el; stage = 5
print(f"t={el:7.2f}s >>> (B) PRESSED", flush=True)
elif stage == 5 and c != "menu":
marks["left_menu"] = el
print(f"t={el:7.2f}s LEFT THE MENU -> {c}, "
f"{el-marks['B']:.2f}s after (B)", flush=True)
stage = 6
elif stage == 6 and el > marks["left_menu"] + 12:
break
p.kill()
with open(f"{OUT}/trace.tsv", "w") as f:
f.write("t_s\tscreen\tcorr_title\tcorr_menu\tglyph\n")
for r in rows:
f.write(f"{r[0]:.3f}\t{r[1]}\t{r[2]:.4f}\t{r[3]:.4f}\t{r[4]}\n")
print("\nmarks:", {k: round(v, 2) for k, v in marks.items()})
if "title_art" in marks and "plate" in marks:
print(f"PLATE DELAY: {marks['plate']-marks['title_art']:.2f}s "
f"after the title art first matched")
if "B" in marks and "left_menu" in marks:
print(f"(B) -> left the menu in {marks['left_menu']-marks['B']:.2f}s")
print(f"trace: {OUT}/trace.tsv")
return 0
if __name__ == "__main__":
sys.exit(main())