1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
#!/usr/bin/env python3
"""P74(启动开场过渡:水墨式渐显)证据分析:真实 xvfb 录启动 0-3 秒序列抽帧。
判据(读真实实跑日志 + 逐帧像素统计):
1. 日志含 5 行 `p74-shot=p74-splash-<frame> inkStrength=<v>`,帧号递增 {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 <record-dir> <out-txt>
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())
|