#!/usr/bin/env python3 """P73(昼夜光照层次:四时段档)证据分析:同一桥头机位 6/12/18/22 时四张真实帧。 判据(读真实实跑日志 + 逐帧像素统计): 1. 日志含四行 `p73-shot=p73- hour=.. period=.. blend=.. lantern=.. tint=.. veil=..`, 时段名覆盖 清晨/正午/黄昏/夜晚。 2. 参数一致性:正午 veil=0 且 lantern=0;黄昏 lantern>=0.3(路灯/灯笼开始点亮); 夜晚 lantern>=1.0;四行 veil/tint/blend 组合两两不同。 3. 抽帧 PNG:1280x720、非空白(std>=8)、无 24x24 纯黑块。 4. 可辨差异:逐帧平均亮度(night 最暗、noon 最亮)与平均暖度(黄昏 R-B 最高); 四帧两两像素 diff > 0。 5. 色调是否「好看/自然」的 vision 复核由 Hermes 侧完成,非本脚本判据。 Usage: python3 scripts/analyze-p73.py Exit 0 pass / 1 fail / 2 usage. """ import re import sys from pathlib import Path import numpy as np from PIL import Image SHOT_RE = re.compile( r"^p73-shot=(\S+) hour=([\d.]+) tick=(\d+) period=(\S+) blend=([\d.]+) lantern=([\d.]+) tint=(\d+),(\d+),(\d+) veil=(\d+)$", re.MULTILINE, ) ORDER = ["p73-morning", "p73-noon", "p73-dusk", "p73-night"] DARK_SIDE = 24 DARK_MAX = 8 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 = [ "P73(昼夜光照层次:清晨/正午/黄昏/夜晚四档)证据分析 — 真实 Desktop xvfb 1280x720", "钩子:LV_P73_SHOT=1(同一桥头光源机位,6/12/18/22 各一帧,画面差异只来自光照)", "路径:tick → periodAtTick → periodLighting(环境色调/覆盖色/VeilAlpha/灯光) + 连续 blend/warmth veils", "", ] shots = {} for m in SHOT_RE.finditer(log): name = m.group(1) if name not in shots: shots[name] = { "hour": float(m.group(2)), "tick": int(m.group(3)), "period": m.group(4), "blend": float(m.group(5)), "lantern": float(m.group(6)), "tint": (int(m.group(7)), int(m.group(8)), int(m.group(9))), "veil": int(m.group(10)), } for name in ORDER: if name not in shots: failures.append(f"log missing p73-shot={name}") else: s = shots[name] lines.append( f"{name}: hour={s['hour']} tick={s['tick']} period={s['period']} blend={s['blend']} lantern={s['lantern']} tint={s['tint']} veil={s['veil']}" ) lines.append("") if "p73-noon" in shots: s = shots["p73-noon"] if s["veil"] != 0: failures.append(f"noon veil {s['veil']} != 0 (must be identity)") if s["lantern"] != 0.0: failures.append(f"noon lantern {s['lantern']} != 0") if "p73-dusk" in shots and shots["p73-dusk"]["lantern"] < 0.3: failures.append(f"dusk lantern {shots['p73-dusk']['lantern']} < 0.3 (lamps must start at dusk)") if "p73-night" in shots and shots["p73-night"]["lantern"] < 1.0: failures.append(f"night lantern {shots['p73-night']['lantern']} < 1.0") combos = {(s["blend"], s["lantern"], s["tint"], s["veil"]) for s in shots.values()} if len(combos) < len(shots): failures.append("two periods share identical (blend,lantern,tint,veil) parameters") lines.append("") stats = {} for name in ORDER: path = rec / f"{name}.png" if not path.exists(): failures.append(f"missing frame {name}.png") 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] mean = rgb.mean(axis=(0, 1)) warmth = float(mean[0] - mean[2]) 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), ) stats[name] = {"brightness": brightness, "warmth": warmth, "std": std} lines.append( f"{name}.png: size={w}x{h} brightness={brightness:.1f} R={mean[0]:.1f} G={mean[1]:.1f} B={mean[2]:.1f} warmth(R-B)={warmth:+.1f} std={std:.1f} dark_block={dark}" ) if (w, h) != (1280, 720): failures.append(f"{name}.png size {w}x{h} != 1280x720") if std < 8.0: failures.append(f"{name}.png looks blank (std={std:.1f})") if dark > 0: failures.append(f"{name}.png has {DARK_SIDE}x{DARK_SIDE} pure-black block") lines.append("") have = [n for n in ORDER if n in stats] for i in range(len(have)): for j in range(i + 1, len(have)): a = np.asarray(Image.open(rec / f"{have[i]}.png").convert("RGB"), dtype=np.int16) b = np.asarray(Image.open(rec / f"{have[j]}.png").convert("RGB"), dtype=np.int16) differing = int((np.abs(a - b).sum(axis=2) > 0).sum()) lines.append(f"diff {have[i]} vs {have[j]}: differing_pixels={differing}") if differing == 0: failures.append(f"{have[i]} identical to {have[j]}") if all(n in stats for n in ORDER): if not (stats["p73-dusk"]["warmth"] > stats["p73-noon"]["warmth"]): failures.append("dusk is not warmer than noon (R-B)") if not (stats["p73-night"]["warmth"] < stats["p73-noon"]["warmth"]): failures.append("night is not cooler than noon (R-B)") if not (stats["p73-night"]["brightness"] < stats["p73-dusk"]["brightness"]): failures.append("night is not darker than dusk") if not (stats["p73-night"]["brightness"] < stats["p73-morning"]["brightness"]): failures.append("night is not darker than morning") lines.append("") lines.append( f"brightness order: " + " > ".join(f"{n.split('-')[1]}({stats[n]['brightness']:.1f})" for n in sorted(ORDER, key=lambda n: -stats[n]["brightness"])) ) lines.append( f"warmth order: " + " > ".join(f"{n.split('-')[1]}({stats[n]['warmth']:+.1f})" for n in sorted(ORDER, key=lambda n: -stats[n]["warmth"])) ) lines.append("") if failures: lines.append("verdict=FAIL") for f in failures: lines.append(" - " + f) else: lines.append("verdict=PASS") lines.append(" four periods have distinct parameters; noon identity, dusk lamps on, night lamps full;") lines.append(" real frames render 1280x720, differ pairwise, night darker / noon brighter / dusk warmer.") 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())