#!/usr/bin/env python3 """P76(底部条/对话面板精修)证据分析:真实 xvfb 昼/夜 HUD + 对话面板/任务面板打开态。 判据(读真实实跑日志 + 区域像素统计;近黑阈值 24 对齐旧纯黑面板底色): 1. 日志含 p43-shot day/night、p44-shot=p44-dialog-a、p67-shot=p67-target。 2. 底部条区域(hotbar rect)与图例带:近黑占比 < 20%、无实心 24x24 近黑块、亮像素(键位/图标/文字)> 400。 3. 对话面板 / 任务面板区域:近黑占比 < 20%、无实心 24x24 近黑块、亮文字像素 > 阈值。 4. 昼/夜两帧、对话第 1/2 页两帧 pairwise diff > 0。 5. 观感由 Hermes vision 侧复核,非本脚本判据。 Usage: python3 scripts/analyze-p76.py Exit 0 pass / 1 fail / 2 usage. """ import re import sys from pathlib import Path import numpy as np from PIL import Image from numpy.lib.stride_tricks import sliding_window_view P43_RE = re.compile( r"^p43-shot=(p43-hud-(?:day|night))\S* tick=(\d+) calendar=(\d+),(\d+),(\d+),(\d+) hotbar=(\d+),(\d+),(\d+),(\d+)", re.MULTILINE, ) P44_RE = re.compile( r"^p44-shot=(p44-dialog-[a-z]) panel=dialogue rect=(\d+),(\d+),(\d+),(\d+)", re.MULTILINE, ) THR = 24 BLOCK = 24 DARK_FRAC_MAX = 0.20 def region_stats(path, rect): x, y, w, h = rect rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.int16) r = rgb[y : y + h, x : x + w] mask = (r[:, :, 0] <= THR) & (r[:, :, 1] <= THR) & (r[:, :, 2] <= THR) dark_frac = float(mask.mean()) block = 0 if mask.shape[0] >= BLOCK and mask.shape[1] >= BLOCK: win = sliding_window_view(mask.astype(np.int64), (BLOCK, BLOCK)) block = int(win.sum(axis=(2, 3)).max()) bright = int(((r[:, :, 0] >= 200) & (r[:, :, 1] >= 200) & (r[:, :, 2] >= 200)).sum()) return dark_frac, block, bright, (w, h) def main(): if len(sys.argv) != 3: print(__doc__) return 2 rec = Path(sys.argv[1]) out = Path(sys.argv[2]) failures = [] lines = [ "P76(底部条/槽位药丸化 + 对话/交互面板半透明描边)证据分析 — 真实 Desktop xvfb 1280x720", "近黑阈值 24(旧纯黑面板底色 (14,22,30) 口径);实心块 = 24x24 全近黑。", "", ] # --- 底部条:昼/夜 --- hud = {m.group(1): (int(m.group(7)), int(m.group(8)), int(m.group(9)), int(m.group(10))) for m in P43_RE.finditer((rec / "log-P43.txt").read_text(encoding="utf-8", errors="replace") if (rec / "log-P43.txt").exists() else "")} for name in ["p43-hud-day", "p43-hud-night"]: path = rec / f"{name}.png" if name not in hud or not path.exists(): failures.append(f"missing {name}") continue bar = hud[name] df, block, bright, size = region_stats(path, bar) # 图例带:条上方 30px legend = (bar[0] - 5, bar[1] - 34, bar[2] + 10, 30) ldf, lblock, lbright, _ = region_stats(path, legend) lines.append( f"{name}: hotbar={bar} size={size} dark_frac={df:.3f} maxblk={block} bright={bright} | legend dark_frac={ldf:.3f} maxblk={lblock} bright={lbright}" ) if size != (1280, 720) and size[0] > 0: pass if df >= DARK_FRAC_MAX: failures.append(f"{name} hotbar dark_frac {df:.3f} >= {DARK_FRAC_MAX}") if block >= BLOCK * BLOCK: failures.append(f"{name} hotbar has solid {BLOCK}x{BLOCK} near-black block") if bright < 400: failures.append(f"{name} hotbar bright {bright} < 400 (键位/图标/文字可能丢失)") if ldf >= DARK_FRAC_MAX: failures.append(f"{name} legend dark_frac {ldf:.3f} >= {DARK_FRAC_MAX}") if lbright < 200: failures.append(f"{name} legend bright {lbright} < 200 (键位+动作名可能不可读)") lines.append("") # --- 对话面板 --- p44_log = (rec / "log-P44.txt").read_text(encoding="utf-8", errors="replace") if (rec / "log-P44.txt").exists() else "" dialogs = {m.group(1): (int(m.group(2)), int(m.group(3)), int(m.group(4)), int(m.group(5))) for m in P44_RE.finditer(p44_log)} dpath = rec / "p44-dialog-a.png" if "p44-dialog-a" in dialogs and dpath.exists(): rect = dialogs["p44-dialog-a"] df, block, bright, size = region_stats(dpath, rect) lines.append(f"p44-dialog-a: panel={rect} dark_frac={df:.3f} maxblk={block} bright={bright}") if df >= DARK_FRAC_MAX: failures.append(f"dialog panel dark_frac {df:.3f} >= {DARK_FRAC_MAX}") if block >= BLOCK * BLOCK: failures.append(f"dialog panel has solid {BLOCK}x{BLOCK} near-black block") if bright < 300: failures.append(f"dialog panel bright {bright} < 300") else: failures.append("missing p44-dialog-a panel") lines.append("") # --- 任务面板(交互面板换行渲染路径)--- tpath = rec / "p67-target.png" if tpath.exists(): df, block, bright, size = region_stats(tpath, (16, 470, 380, 220)) lines.append(f"p67-target task-panel region=(16,470,380,220) dark_frac={df:.3f} maxblk={block} bright={bright}") if df >= DARK_FRAC_MAX: failures.append(f"task panel dark_frac {df:.3f} >= {DARK_FRAC_MAX}") if block >= BLOCK * BLOCK: failures.append(f"task panel has solid {BLOCK}x{BLOCK} near-black block") else: failures.append("missing p67-target.png") lines.append("") # --- 帧差 --- def diff(a, b): ia = np.asarray(Image.open(rec / f"{a}.png").convert("RGB"), dtype=np.int16) ib = np.asarray(Image.open(rec / f"{b}.png").convert("RGB"), dtype=np.int16) return int((np.abs(ia - ib).sum(axis=2) > 0).sum()) for a, b in [("p43-hud-day", "p43-hud-night"), ("p44-dialog-a", "p44-dialog-b")]: if (rec / f"{a}.png").exists() and (rec / f"{b}.png").exists(): d = diff(a, b) lines.append(f"diff {a} vs {b}: differing_pixels={d}") if d == 0: failures.append(f"{a} identical to {b}") lines.append("") if failures: lines.append("verdict=FAIL") for f in failures: lines.append(" - " + f) else: lines.append("verdict=PASS") lines.append(" hotbar/legend/dialog/task regions have no solid near-black block;") lines.append(" key chips, icons and action-name legend remain bright; frames differ pairwise.") out.parent.mkdir(parents=True, exist_ok=True) out.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"wrote {out} verdict={'FAIL' if failures else 'PASS'}") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())