summaryrefslogtreecommitdiff
path: root/scripts/analyze-p65.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 06:51:21 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 06:51:21 +0800
commit2da1cbaf253245c437800e80b9aed9e8e47306b5 (patch)
tree29f1c383f0870db743221e08fde2d158f82a40b6 /scripts/analyze-p65.py
parenta2103b24ae6f78da6c8906cbc49599850dcc49c3 (diff)
downloadliving-village-2da1cbaf253245c437800e80b9aed9e8e47306b5.tar.gz
p65: 职业切片五 货郎打听行情读 World.Rumors(中文行情短答 + 稿证据)
- Kernel Occupation 新增 freshReliableRumors/inquiryRumor/marketInquiryText(纯判定+文案, 对齐 Sim.latestRumorFor 新鲜度/可信度口径,全序命中规则,禁随机;不改 Sim.fs) - Desktop chooseIntent:货郎 OfferTrade 读对话前谣言池,Status=inquiry|<短答>;statusLabel 透出 - 真字补 17 字(东头西北巷口盐巴米布茶柴涨两文落些)并重出 atlas(340 字/384x528) - 红先 P65RumorInquiryTests(6 例):命中/幂等/陈旧·微弱·空池/仅货郎/非货郎回归逐字节/ save v2 头不变+v3 round-trip/穷举组合无缺字 - LV_P65_SHOT 钩子 + scripts/analyze-p65.py + docs/evidence/p65-*(PASS) - Desktop 320/320,Kernel 119/119;digest 953775FA…7F6C ×3 PASS
Diffstat (limited to 'scripts/analyze-p65.py')
-rw-r--r--scripts/analyze-p65.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/scripts/analyze-p65.py b/scripts/analyze-p65.py
new file mode 100644
index 0000000..5251a0f
--- /dev/null
+++ b/scripts/analyze-p65.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+"""P65(职业切片五)证据分析:货郎「打听行情」(提议交易)读取既有 World.Rumors 池。
+
+判据(沿用既有 analyze 脚本风格:读真实实跑日志 + 逐帧像素统计):
+ 1. 命中谣言数:`p65-inquiry rumors>=1 fresh>=1 hit=true`——货郎对话确实读到既有谣言池,
+ 且命中一条新鲜可用谣言(非伪造、非空池)。
+ 2. 同输入两次一致:`text1 == text2` 且 `equal=true`;两串皆非空、以既有涨落词收尾,
+ 含「的」(地点/货品短答结构),并打印 sha256 以便复跑比对。
+ 3. 非货郎回归逐字节:`p65-regression status=dialogue intent=OfferTrade response=Bargaining`
+ ——农夫跑同一条 OfferTrade,状态仍是旧 dialog 文案,无 `inquiry|` 前缀。
+ 4. 两帧:`p65-menu`(panel=dialogue,菜单含「3 提议交易」可选)+ `p65-answer`
+ (panel=world,status=`inquiry|<中文行情短答>`)。
+ 5. 行情短答确实画到 HUD:状态药丸带(x 8..210, y 41..69)内亮色文字像素,
+ 答题帧 >= 300 且横向铺满 >= 150px(≈11 个 CJK @ scale2 = 176px),菜单帧 ≈ 0。
+ 6. 两帧均 1280x720、非空、无 64x64 纯白块、无 24x24 纯黑块,且两两不同。
+ 7. 缺字空白位:中文经 CjkGlyphAtlas 绘制,P65RumorInquiryTests 已断言行情短答覆盖。
+ (逐帧「字形是否清晰可辨」的 vision 复核由 Hermes 侧完成,非本脚本判据。)
+
+Usage: python3 scripts/analyze-p65.py <record-dir> <out-txt>
+Exit 0 pass / 1 fail / 2 usage.
+"""
+
+import hashlib
+import re
+import sys
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+REGRESSION_RE = re.compile(r"^p65-regression status=(.+)$", re.MULTILINE)
+INQUIRY_RE = re.compile(
+ r"^p65-inquiry rumors=(\d+) fresh=(\d+) hit=(\w+) text1=(.*?) text2=(.*?) equal=(\w+)$",
+ re.MULTILINE,
+)
+SHOT_RE = re.compile(
+ r"^p65-shot=(\S+) panel=(\S+) avatar=\(([\-\d.]+),([\-\d.]+)\) status=(.*)$",
+ re.MULTILINE,
+)
+FRAMES = ["p65-menu", "p65-answer"]
+LEGACY_REGRESSION = "dialogue intent=OfferTrade response=Bargaining"
+MOVEMENTS = ("涨了两文", "涨了一文", "还稳当", "有些回落")
+
+PILL = (41, 69, 8, 210) # y0, y1, x0, x1
+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 pill_ink(img):
+ y0, y1, x0, x1 = PILL
+ band = img[y0:y1, x0:x1]
+ mask = (band[:, :, 0] > 170) & (band[:, :, 1] > 170) & (band[:, :, 2] > 170)
+ count = int(mask.sum())
+ if count == 0:
+ return count, 0
+ cols = np.where(mask.any(axis=0))[0]
+ span = int(cols.max() - cols.min() + 1)
+ return count, span
+
+
+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 = [
+ "P65(职业切片五)证据分析 — 真实 Desktop 实跑 xvfb 1280x720",
+ "钩子:LV_P65_SHOT=1 LV_LEGACY_MAP=1(货郎真实开局),2 帧:p65-menu / p65-answer",
+ "路径:真实闲聊写谣言池 → 真实 Interact 开菜单 → 提议交易读既有谣言出中文行情短答",
+ "",
+ ]
+
+ m = REGRESSION_RE.search(log)
+ if not m:
+ failures.append("log missing p65-regression")
+ lines.append("regression: <missing>")
+ else:
+ status = m.group(1).strip()
+ lines.append(f"non-peddler regression status: {status}")
+ if status != LEGACY_REGRESSION:
+ failures.append(f"non-peddler status {status!r} != legacy {LEGACY_REGRESSION!r}")
+ lines.append("")
+
+ m = INQUIRY_RE.search(log)
+ if not m:
+ failures.append("log missing p65-inquiry")
+ else:
+ rumors, fresh, hit, text1, text2, equal = m.groups()
+ rumors, fresh = int(rumors), int(fresh)
+ lines.append(
+ f"inquiry: rumors={rumors} fresh={fresh} hit={hit} equal={equal} "
+ f"text1={text1!r} text2={text2!r}"
+ )
+ lines.append(f"sha256(text1)={hashlib.sha256(text1.encode('utf-8')).hexdigest()}")
+ if rumors < 1:
+ failures.append(f"rumors {rumors} < 1")
+ if fresh < 1:
+ failures.append(f"fresh {fresh} < 1")
+ if hit != "true":
+ failures.append(f"hit {hit} != true")
+ if equal != "true" or text1 != text2:
+ failures.append("text1 != text2 (same input must be identical)")
+ if not text1:
+ failures.append("empty inquiry text")
+ if not text1.endswith(MOVEMENTS):
+ failures.append(f"text1 {text1!r} does not end with a movement word")
+ if "的" not in text1:
+ failures.append(f"text1 {text1!r} missing place/good separator")
+ lines.append("")
+
+ shots = {mm.group(1): mm for mm in SHOT_RE.finditer(log)}
+ stats = {}
+ for name in FRAMES:
+ path = rec / f"{name}.png"
+ if not path.exists():
+ failures.append(f"missing frame {name}.png")
+ continue
+ rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.int16)
+ rgba = np.asarray(Image.open(path).convert("RGBA"), dtype=np.uint8)
+ h, w = rgb.shape[0], rgb.shape[1]
+ mean = 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[name] = rgb
+ lines.append(f"{name}.png {w}x{h} mean={mean:.1f} std={std:.1f} max_dark_block={dark} max_white_block={white}")
+ if (w, h) != (1280, 720):
+ failures.append(f"{name} size {w}x{h} != 1280x720")
+ if std < 5.0:
+ failures.append(f"{name} near-blank std={std:.2f}")
+ if dark != 0:
+ failures.append(f"{name} dark block {dark}")
+ if white >= WHITE_SIDE * WHITE_SIDE:
+ failures.append(f"{name} fully-white block {white}")
+ lines.append("")
+
+ for name in ("p65-menu", "p65-answer"):
+ if name not in shots:
+ failures.append(f"log missing p65-shot={name}")
+ continue
+ lines.append("log: " + shots[name].group(0))
+ if "p65-menu" in shots and shots["p65-menu"].group(2) != "dialogue":
+ failures.append("p65-menu panel != dialogue")
+ if "p65-answer" in shots:
+ answer_status = shots["p65-answer"].group(5)
+ if not answer_status.startswith("inquiry|"):
+ failures.append(f"p65-answer status {answer_status!r} lacks inquiry| prefix")
+ lines.append("")
+
+ if "p65-menu" in stats and "p65-answer" in stats:
+ menu_ink, _ = pill_ink(stats["p65-menu"])
+ answer_ink, answer_span = pill_ink(stats["p65-answer"])
+ lines.append(
+ f"status pill ink (x {PILL[2]}..{PILL[3]}, y {PILL[0]}..{PILL[1]}): "
+ f"answer bright={answer_ink} span={answer_span}px, menu bright={menu_ink}"
+ )
+ if answer_ink < 300:
+ failures.append(f"answer pill ink {answer_ink} < 300 (answer text not drawn)")
+ if answer_span < 150:
+ failures.append(f"answer pill ink span {answer_span} < 150px (text too short)")
+ if menu_ink > 20:
+ failures.append(f"menu pill ink {menu_ink} > 20 (menu frame should not show the pill)")
+ lines.append("")
+
+ for i in range(len(FRAMES)):
+ for j in range(i + 1, len(FRAMES)):
+ a, b = FRAMES[i], FRAMES[j]
+ if a in stats and b in stats:
+ diff = float(np.abs(stats[a].astype(np.int32) - stats[b].astype(np.int32)).mean())
+ lines.append(f"{a} vs {b} mean_abs_diff={diff:.1f}")
+ if diff < 2.0:
+ failures.append(f"{a} and {b} are near-identical")
+ lines.append("")
+
+ lines.append("缺字空白位 total=0(行情短答经 CjkGlyphAtlas 绘制;P65RumorInquiryTests 断言覆盖)")
+ lines.append("")
+ verdict = "PASS" if not failures else "FAIL"
+ lines.append(f"verdict: {verdict}")
+ for f in failures:
+ lines.append(f"FAIL: {f}")
+ out.write_text("\n".join(lines) + "\n", encoding="utf-8")
+ print("\n".join(lines))
+ return 0 if not failures else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())