summaryrefslogtreecommitdiff
path: root/scripts/analyze-p44-dialog.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 19:29:27 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 19:29:27 +0800
commite28b3c102f2a0b387a4df84f053306d71e5e41ba (patch)
tree0b3b13b10bba4283dde3aba60cc0ec42495d6b82 /scripts/analyze-p44-dialog.py
parentd671335d687496f393dbad63950707cabbf24e5b (diff)
downloadliving-village-e28b3c102f2a0b387a4df84f053306d71e5e41ba.tar.gz
p44: 对话框/文字弹层 — 完整背景框+中文断行换行+分页翻页+Esc关闭+输入隔离(原创32x64点阵头像)
Diffstat (limited to 'scripts/analyze-p44-dialog.py')
-rw-r--r--scripts/analyze-p44-dialog.py137
1 files changed, 137 insertions, 0 deletions
diff --git a/scripts/analyze-p44-dialog.py b/scripts/analyze-p44-dialog.py
new file mode 100644
index 0000000..c9405e0
--- /dev/null
+++ b/scripts/analyze-p44-dialog.py
@@ -0,0 +1,137 @@
+#!/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 <record-dir> <shot-log> <out-txt>
+
+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))