From 837fd81a2bd857011855ca1dc32e646629f14236 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 29 Sep 2026 11:08:32 +0800 Subject: p74: 启动开场过渡(水墨式渐显)+ 开始界面布局回归 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增纯函数 InkReveal.fs(inkStrength ease-out 消散 + 6 滴预乘墨滴 dabs),DrawSplash 在场景层之上、标题之下绘制,与既有 reveal/veil 淡入叠加,形成可辨识水墨开场;单滴 alpha<255 且椭圆行落成,无实心黑块。开始界面沿用既有 开始/读档/退出(有档 继续)。新增 P74LaunchTransitionTests(4 例)与 LV_P74_SHOT/-analyze-p74 证据(5 帧墨幕非增、两两 diff>0,verdict=PASS)。HUD 结构未动;Desktop 350/350、Kernel 120/120、digest 与 p70 一致。 --- scripts/analyze-p74.py | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 scripts/analyze-p74.py (limited to 'scripts/analyze-p74.py') diff --git a/scripts/analyze-p74.py b/scripts/analyze-p74.py new file mode 100644 index 0000000..4569791 --- /dev/null +++ b/scripts/analyze-p74.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""P74(启动开场过渡:水墨式渐显)证据分析:真实 xvfb 录启动 0-3 秒序列抽帧。 + +判据(读真实实跑日志 + 逐帧像素统计): + 1. 日志含 5 行 `p74-shot=p74-splash- inkStrength=`,帧号递增 {8,30,60,100,140}。 + 2. inkStrength 随帧非增,且在 dissolveFrames(96) 后为 0(墨幕确实在消散)。 + 3. 抽帧 PNG:1280x720、非空白(std>=6)、无 24x24 纯黑块(标题/屋脊的纯白属预期,仅报告不判失败)。 + 4. 动画在变:任意两帧 pixel diff > 0(且首末帧 diff 显著)。 + 5. 画面「是否好看/水墨味」的 vision 复核由 Hermes 侧完成,非本脚本判据。 + +Usage: python3 scripts/analyze-p74.py +Exit 0 pass / 1 fail / 2 usage. +""" + +import itertools +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +SHOT_RE = re.compile(r"^p74-shot=p74-splash-(\d+) inkStrength=([\d.]+)$", re.MULTILINE) +EXPECTED = [8, 30, 60, 100, 140] +DARK_SIDE = 24 +DARK_MAX = 8 +WHITE_SIDE = 64 +WHITE_MIN = 250 + + +def max_block(rgba, side, predicate): + h, w = rgba.shape[0], rgba.shape[1] + if h < side or w < side: + return 0 + mask = predicate(rgba).astype(np.int64) + integral = np.zeros((h + 1, w + 1), dtype=np.int64) + integral[1:, 1:] = mask.cumsum(axis=0).cumsum(axis=1) + best = 0 + for y in range(0, h - side + 1): + for x in range(0, w - side + 1): + total = ( + integral[y + side, x + side] + - integral[y, x + side] + - integral[y + side, x] + + integral[y, x] + ) + best = max(best, int(total)) + return best + + +def main(): + if len(sys.argv) != 3: + print(__doc__) + return 2 + rec = Path(sys.argv[1]) + out = Path(sys.argv[2]) + log = (rec / "log.txt").read_text(encoding="utf-8", errors="replace") + failures = [] + lines = [ + "P74(启动开场过渡:水墨式渐显)证据分析 — 真实 Desktop xvfb 1280x720", + "钩子:LV_P74_SHOT=1(60Hz 固定步进,splash 帧表 8/30/60/100/140 各一帧)", + "路径:splashFrame → InkReveal.inkStrength/dabs(预乘墨滴)→ 露出天色/水面/屋脊/标题", + "", + ] + + shots = [(int(m.group(1)), float(m.group(2))) for m in SHOT_RE.finditer(log)] + got = [f for f, _ in shots] + lines.append("log shots: " + ", ".join(f"{f}(ink={v:.3f})" for f, v in shots)) + if got != EXPECTED: + failures.append(f"log frames {got} != expected {EXPECTED}") + strengths = [v for _, v in shots] + if strengths != sorted(strengths, reverse=True): + failures.append("inkStrength is not non-increasing across frames") + if not all(v == 0.0 for f, v in shots if f >= 96): + failures.append("inkStrength is not 0 after dissolveFrames (96)") + lines.append("") + + stats = {} + for frame in EXPECTED: + path = rec / f"p74-splash-{frame:03d}.png" + if not path.exists(): + failures.append(f"missing frame {path.name}") + continue + rgba = np.asarray(Image.open(path).convert("RGBA"), dtype=np.uint8) + rgb = rgba[:, :, :3].astype(np.float64) + h, w = rgb.shape[0], rgb.shape[1] + brightness = float(rgb.mean()) + std = float(rgb.std()) + dark = max_block( + rgba, DARK_SIDE, + lambda a: (a[:, :, 0] <= DARK_MAX) & (a[:, :, 1] <= DARK_MAX) & (a[:, :, 2] <= DARK_MAX) & (a[:, :, 3] >= 250), + ) + white = max_block( + rgba, WHITE_SIDE, + lambda a: (a[:, :, 0] >= WHITE_MIN) & (a[:, :, 1] >= WHITE_MIN) & (a[:, :, 2] >= WHITE_MIN) & (a[:, :, 3] >= 250), + ) + stats[frame] = {"brightness": brightness, "std": std} + lines.append(f"p74-splash-{frame:03d}.png: size={w}x{h} brightness={brightness:.1f} std={std:.1f} dark_block={dark} white_block={white}") + if (w, h) != (1280, 720): + failures.append(f"{path.name} size {w}x{h} != 1280x720") + if std < 6.0: + failures.append(f"{path.name} looks blank (std={std:.1f})") + if dark > 0: + failures.append(f"{path.name} has {DARK_SIDE}x{DARK_SIDE} pure-black block") + lines.append("") + + have = [f for f in EXPECTED if f in stats] + for a, b in itertools.combinations(have, 2): + ia = np.asarray(Image.open(rec / f"p74-splash-{a:03d}.png").convert("RGB"), dtype=np.int16) + ib = np.asarray(Image.open(rec / f"p74-splash-{b:03d}.png").convert("RGB"), dtype=np.int16) + differing = int((np.abs(ia - ib).sum(axis=2) > 0).sum()) + lines.append(f"diff frame {a:03d} vs {b:03d}: differing_pixels={differing}") + if differing == 0: + failures.append(f"frame {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(" ink strength decays to 0 across the captured frames; frames render 1280x720,") + lines.append(" no big black blocks, and every pair differs (the opening transition animates).") + 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()) -- cgit v1.2.3