Files
Sylpheed/tools/re-capture/check_labels.py
sylph-decoder 374a8e2523 re: RETRACT the alpha-80 refutation -- it is declared, in ptbtn00f.rat
I claimed ptbtn00f's peak alpha of 80 was capture-only and refuted the
port's audit with it. Verified on the disc myself: ptbtn00f.rat, loop 120,
eight keyframes 0:0 6:6 29:74 35:80 50:80 58:74 97:6 105:0. ptbtn00 carries
TWO child records -- the leaf ptbtn00.rat (flat 255, which I read and
described correctly) and the focus record ptbtn00f.rat, which holds the
pulse. My "nothing declares 80" said only that nothing reached my
enumeration. METHOD.md has five entries on exactly this class.

Resolves the 🟡 with it: the corpus line attributing the 120-unit loop to
ptbtn00f is correct as written.

Turns the retraction into a stronger check. The amplitude is declared, so
check_labels.py now verifies the SHAPE: mean |alpha| error 1.14 levels (f6)
and 0.35 (f6b) against the declared 8-key curve. The first version assumed
the onset frame was t=0 and f6b FAILED at 3.24; the fix was aligning by
content rather than loosening the tolerance, since the 6->74 segment climbs
~6 levels per frame. Best lag is ~1 frame in both runs -- the element is not
drawn while its curve sits at alpha 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-03 20:31:58 +00:00

157 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Re-derive every element LABEL in the F5/F6 findings from captures + disc.
⚠️ WHY THIS EXISTS. Three of my errors were the label, not the measurement:
`0x3003` read as a different role from `0x3002`; `ptcopyright` called "the plate"
in the clock-conflict page; and `ptcopyright` called "the plate" AGAIN in the F6
page, written after that correction. Every number was right. What it was pointed
at was not, and a correction in one document did not reach the next.
sylpheed-port has `check-authored-vs-declared` for values with a declared
counterpart. It cannot cover a value that exists only in a capture and names an
element -- that one rests entirely on my label. This is that case.
Each identification below was originally MADE by matching a declared quantity, so
each is re-derivable. If a label drifts, the agreement it was built on breaks.
python3 check_labels.py # check
python3 check_labels.py --selftest # prove it can fail
"""
import sys, collections
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from read_draws import read
# ptbtn00f.rat, GP_TITLE entries 2/3 -- read off the disc with
# `cargo run -p sylpheed-formats --example leaf_keyframes -- GP_TITLE ptbtn00f.rat 2`
# Declared loop 120 units. This is the FOCUS record; ptbtn00.rat is the leaf and
# is flat 255. Looking only at the leaf is how I wrongly called alpha 80
# undeclared -- an absence claim from a search that did not cover the space.
PTBTN00F = [(0,0),(6,6),(29,74),(35,80),(50,80),(58,74),(97,6),(105,0),(120,0)]
def declared_alpha(t):
t = t % 120
for i in range(len(PTBTN00F)-1):
(t0,a0),(t1,a1) = PTBTN00F[i], PTBTN00F[i+1]
if t0 <= t <= t1:
return a0 if t1==t0 else a0 + (a1-a0)*(t-t0)/(t1-t0)
return 0
CAPS = {'f6': '/sylph-home/re/f6/xenia_re_ui_draws_01.log',
'f6b': '/sylph-home/re/f6b/xenia_re_ui_draws_01.log'}
SWEEP_PAGE = '8154'
PTCOPY_X = (-0.54, 0.54, 0.54, -0.54)
def features(path, swap_plate=False):
fr = read(path); fs = sorted(fr)
f = {}
pairs = []
for fm in fs:
adds = [q for q in fr[fm] if q.blend == '0x01010101' and q.page.startswith(SWEEP_PAGE)]
if len(adds) >= 2: pairs.append((fm, adds[0], adds[1]))
def cycles(idx, sign):
out = []
for i in range(1, len(pairs)):
d = pairs[i][idx].cx - pairs[i-1][idx].cx
if (sign > 0 and d < -1.0) or (sign < 0 and d > 1.0): out.append(pairs[i][0])
return out
ca, cb = cycles(1, +1), cycles(2, -1)
f['pteff03_period'] = ca[1] - ca[0]
f['pteff03a_period'] = cb[1] - cb[0]
import math
def length(q):
pts = [(x*640, y*360) for x, y in q.verts]
return max(math.dist(pts[i], pts[j]) for i in range(4) for j in range(i+1, 4))
f['pteff03_len'] = sum(length(p[1]) for p in pairs) / len(pairs)
f['pteff03a_len'] = sum(length(p[2]) for p in pairs) / len(pairs)
# parent ramp: sweep quad A's alpha climbing to full from its cycle start
start = ca[0]
seq = [(fm, a.alpha) for fm, a, _ in pairs if fm >= start]
f['parent_ramp'] = next(fm for fm, al in seq if al >= 239) - start
# ptcopyright: fade-in length of the -0.54..0.54 quad
cop = [(fm, q.alpha) for fm in fs for q in fr[fm]
if tuple(round(v[0], 2) for v in q.verts) == PTCOPY_X]
f['ptcopyright_ramp'] = next(fm for fm, a in cop if a >= 255) - cop[0][0]
# ptbtn00f: the slot that comes and goes
slots = collections.defaultdict(list)
for fm in fs:
for q in fr[fm]: slots[(q.page[:4], round(q.cx, 2))].append(fm)
cands = []
for k, v in slots.items():
if len(v) < 200: continue
gaps = sum(1 for i in range(1, len(v)) if v[i]-v[i-1] > 1)
if gaps >= 8: cands.append((gaps, k, v))
cands.sort(reverse=True)
if swap_plate and len(cands) > 0: # selftest: point the label at ptcopyright instead
f['pulse_period'] = f['ptcopyright_ramp']
else:
v = cands[0][2]
ons = [v[0]] + [v[i] for i in range(1, len(v)) if v[i]-v[i-1] > 1]
per = sorted(ons[i]-ons[i-1] for i in range(1, len(ons)))
f['pulse_period'] = per[len(per)//2]
# AMPLITUDE: predict the drawn alpha from ptbtn00f.rat's declared curve
key = cands[0][1]
upf = 120.0 / f['pulse_period'] # title units per frame, from the period
start = ons[1] if len(ons) > 1 else ons[0]
obs = []
for fm in range(start, start + f['pulse_period']):
got = [q.alpha for q in fr.get(fm, []) if (q.page[:4], round(q.cx,2)) == key]
if got: obs.append((fm-start, got[0]))
# ⚠️ ALIGN BY CONTENT, not by assuming the onset frame is t=0. The 6->74
# segment climbs ~6 alpha levels per FRAME, so half a frame of phase error
# alone produces ~3 levels of mean error. Search the lag; the lag is a
# measurement, not an error (TEMPORAL-VERIFICATION.md).
best = (99.0, None)
lag = 0.0
while lag < 4.0:
e = sum(abs(a - declared_alpha((k+lag)*upf)) for k, a in obs)/len(obs) if obs else 99.0
if e < best[0]: best = (e, lag)
lag += 0.05
f['pulse_amp_err'], f['pulse_lag'] = best
if swap_plate:
f['pulse_amp_err'] = 99.0
return f
# label -> (derived ratio, declared value, tolerance, what the label asserts)
def checks(f):
return [
("pteff03a is the 720-unit leaf (not a second copy of the 600)",
f['pteff03a_period']/f['pteff03_period'], 720/600, 0.05),
("pteff03a is the sy=800 strip, pteff03 the sy=600",
f['pteff03a_len']/f['pteff03_len'], 800/600, 0.06),
("the pulsing slot is ptbtn00f (120-unit loop vs the sweep's 600 leaf units)",
f['pulse_period']/f['pteff03_period'], 0.1, 0.05),
("the -0.54 quad is ptcopyright (22-unit ramp vs the parent's 30)",
f['ptcopyright_ramp']/f['parent_ramp'], 22/30, 0.08),
("the pulse AMPLITUDE matches ptbtn00f.rat's declared 8-key curve (peak 80)",
f['pulse_amp_err'], 0.0, None),
]
def run(swap=False):
bad = 0
for name, path in CAPS.items():
f = features(path, swap_plate=swap)
print(f" {name}:")
for label, got, want, tol in checks(f):
if tol is None: # absolute: mean |alpha| error, <=3 levels
err, ok = got, got <= 3.0
print(f" [{'PASS' if ok else 'FAIL'}] mean |alpha| error {got:5.2f} levels (tol 3.00, best lag {f.get('pulse_lag',0):.2f} fr) {label}")
bad += not ok
continue
err = abs(got-want)/want
ok = err <= tol
bad += not ok
print(f" [{'PASS' if ok else 'FAIL'}] {got:.4f} vs {want:.4f} ({err*100:4.1f}%, tol {tol*100:.0f}%) {label}")
return bad
if __name__ == '__main__':
if '--selftest' in sys.argv:
print("SELFTEST — the plate label deliberately pointed at ptcopyright.")
print("A check that cannot fail here would not have caught the real error.\n")
bad = run(swap=True)
print(f"\n{'OK: mislabel detected' if bad else 'BROKEN: mislabel NOT detected'} ({bad} failures)")
sys.exit(0 if bad else 1)
print("Element labels in the F5/F6 findings, re-derived from captures + disc:\n")
bad = run()
print(f"\n{'all labels agree' if not bad else str(bad)+' LABEL(S) DRIFTED'}")
sys.exit(1 if bad else 0)