#!/usr/bin/env python3 """P44 dialog-overlay analysis. Reads the `LV_P44_SHOT=1` frames + shot log and writes docs/evidence/p44-analysis.txt with measured numbers: 1. overlay rect / page / wrapped line count / avatar flag for the open frames; 2. the accent top border really exists inside the overlay rect (frame a/b); 3. the overlay rect is gone after Esc (frame c): accent border pixels ~0 and the region differs from frame a, and the world prompt is restored. Usage: python3 scripts/analyze-p44-dialog.py Exit 0 when checks pass, 1 when they fail, 2 on usage/read error. """ import re import sys from pathlib import Path import numpy as np from PIL import Image WIDTH = 1280 HEIGHT = 720 ACCENT_DAY = np.array([212, 168, 96]) OPEN_RE = re.compile(r"p44-shot=(\S+) panel=(\S+) rect=(\d+),(\d+),(\d+),(\d+) page=(\d+)/(\d+) lines=(\d+) avatar=(\w+)") GONE_RE = re.compile(r"p44-shot=(\S+) panel=(\S+) overlay=gone prompt=(.+)") def load(record_dir, name): return np.asarray(Image.open(record_dir / f"{name}.png").convert("RGB"), dtype=int) def accent_border_px(img, rect): x, y, w, _ = rect strip = img[y:y + 3, x:x + w] return int((np.abs(strip - ACCENT_DAY).max(axis=2) <= 16).sum()) def text_bands(img, rect, show_avatar): x, y, w, h = rect text_x = x + 16 + (32 + 12 if show_avatar else 0) # body rows only (below the header), inside the frame. body = img[y + 30:y + h - 20, text_x:x + w - 16] bright = (body.max(axis=2) >= 180).any(axis=1) bands = 0 previous = False for row in bright: if row and not previous: bands += 1 previous = row return bands def main(argv): if len(argv) != 4: sys.stderr.write(__doc__) return 2 record_dir = Path(argv[1]) log_text = Path(argv[2]).read_text(encoding="utf-8") opens = [] gone = [] for line in log_text.splitlines(): m = OPEN_RE.search(line) if m: opens.append( { "name": m.group(1), "panel": m.group(2), "rect": tuple(int(m.group(i)) for i in range(3, 7)), "page": f"{m.group(7)}/{m.group(8)}", "lines": int(m.group(9)), "avatar": m.group(10) == "true", } ) g = GONE_RE.search(line) if g: gone.append({"name": g.group(1), "panel": g.group(2), "prompt": g.group(3)}) if not opens or not gone: sys.stderr.write("need open frames and one overlay=gone frame\n") return 1 failures = [] lines = ["P44 dialog-overlay analysis (LV_P44_SHOT=1)", ""] reference_rect = None reference_img = None for shot in opens: img = load(record_dir, shot["name"]) reference_rect = shot["rect"] if reference_rect is None else reference_rect reference_img = img if reference_img is None else reference_img x, y, w, h = shot["rect"] if not (x >= 0 and y >= 0 and x + w <= WIDTH and y + h <= HEIGHT): failures.append(f"{shot['name']} rect out of viewport") border = accent_border_px(img, shot["rect"]) bands = text_bands(img, shot["rect"], shot["avatar"]) lines.append(f"[{shot['name']}] panel={shot['panel']} rect=({x},{y},{w}x{h}) page={shot['page']} " f"log_lines={shot['lines']} avatar={shot['avatar']}") lines.append(f" rendered_text_bands={bands} accent_border_px={border}") if border < 500: failures.append(f"{shot['name']} overlay accent border missing ({border})") if bands < 1: failures.append(f"{shot['name']} no wrapped text rendered") if shot["lines"] > 4: failures.append(f"{shot['name']} page has more than linesPerPage lines") for shot in gone: img = load(record_dir, shot["name"]) border = accent_border_px(img, reference_rect) x, y, w, h = reference_rect delta = float(np.abs(img[y:y + h, x:x + w].astype(float) - reference_img[y:y + h, x:x + w].astype(float)).mean()) lines.append(f"[{shot['name']}] panel={shot['panel']} prompt={shot['prompt']!r} " f"(rect from {opens[0]['name']})") lines.append(f" accent_border_px={border} region_mean_abs_diff_vs_open={delta:.1f}") if border > 50: failures.append(f"{shot['name']} overlay rect still present after close (border={border})") if delta < 5.0: failures.append(f"{shot['name']} region unchanged vs open, overlay may persist") if "E " not in shot["prompt"]: failures.append(f"{shot['name']} world prompt not restored") lines.append("") lines.append("verdict = " + ("PASS" if not failures else "FAIL")) out_txt = Path(argv[3]) out_txt.parent.mkdir(parents=True, exist_ok=True) out_txt.write_text("\n".join(lines) + "\n", encoding="utf-8") sys.stdout.write("\n".join(lines) + "\n") if failures: for f in failures: sys.stderr.write(f"FAIL {f}\n") return 1 return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))