summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 10:58:24 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 10:58:24 +0800
commit0b1330de9b8d78f398753d878f495f324617d203 (patch)
tree298710d868c5a965c4dfb94fa5db89a796ca78c0 /scripts
parentd170af8485ad787cbc2a87b352d2b5cd81605b4d (diff)
downloadliving-village-0b1330de9b8d78f398753d878f495f324617d203.tar.gz
p73: 昼夜光照四时段档(清晨/正午/黄昏/夜晚)
M6Presentation 新增 periodAtTick/periodLighting 参数表(AmbientTint 乘性色调 + Veil/VeilAlpha 加性覆盖 + LanternGlow),边界与 timePeriodLabel 一致;正午为恒等档不改白平衡。DrawWorldView 并入时段色调与 effectiveLanternGlow(黄昏 0.6 起点亮、夜晚 1.0),覆盖色最后叠加。新增 P73 回归;新增 LV_P73_SHOT 同机位四帧证据 + analyze-p73(verdict=PASS);字形表补 正/午(345)。Desktop 346/346、Kernel 120/120、digest 与 p70 一致。
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p73.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/scripts/analyze-p73.py b/scripts/analyze-p73.py
new file mode 100644
index 0000000..31595bf
--- /dev/null
+++ b/scripts/analyze-p73.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+"""P73(昼夜光照层次:四时段档)证据分析:同一桥头机位 6/12/18/22 时四张真实帧。
+
+判据(读真实实跑日志 + 逐帧像素统计):
+ 1. 日志含四行 `p73-shot=p73-<period> 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 <record-dir> <out-txt>
+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())