Initial commit: 自然记忆站:12 页纯标准库 Python 静态站点生成器,含真实评测台账(108 条)与全站客户端检索

This commit is contained in:
WpyQwq
2026-09-19 11:21:45 +08:00
commit 1da4b14c19
44 changed files with 9624 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>gate</title>
<style>html,body{margin:0;background:#111}iframe{width:100%;height:100vh;border:0;visibility:hidden;position:absolute;inset:0}</style>
</head>
<body>
<!-- 临时闸门检查页,跑完即删,不随站点部署 -->
<iframe id="f"></iframe>
<script>
var W = (location.search.match(/w=(\d+)/) || [0, "1576"])[1];
var LIST = (location.search.match(/p=([^&]+)/) || [0, ""])[1];
var PAGES = LIST ? decodeURIComponent(LIST).split(",") : ["/"];
var out = ["W" + W, "N" + PAGES.length];
function log(k, v) { out.push(k + "=" + v); }
function q(s, r) { return (r || document).querySelector(s); }
function qa(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
var f = document.getElementById("f");
var steps = [];
function run(fn, wait) { steps.push([fn, wait]); }
function next() {
if (!steps.length) { (new Image()).src = "/__gate?d=" + encodeURIComponent(out.join(" ; ")); return; }
var s = steps.shift();
try { s[0](); } catch (e) { log("STEPERR", e.message); }
setTimeout(next, s[1]);
}
function rgb2key(s) {
var m = String(s).match(/rgba?\(([^)]+)\)/);
if (!m) return null;
var p = m[1].split(",").map(function (x) { return parseFloat(x); });
if (p.length > 3 && p[3] === 0) return null;
return Math.round(p[0] / 16) + "," + Math.round(p[1] / 16) + "," + Math.round(p[2] / 16);
}
function gatePage(slug) {
var d = f.contentDocument, w = f.contentWindow;
var VW = w.innerWidth, VH = w.innerHeight;
var all = qa("body *", d);
/* --- 字号刻度:最大渲染字号 vs 正文众数 --- */
var dispMax = 0, dispEl = "-", sizes = {};
/* 只看「自己直接带的文字」,不能因为元素有子元素就跳过 ——
否则 <h1>字<br>字</h1> 和 <span class="mega">100.00<small>%</small></span>
都会被漏掉,最大的字号就量错了。 */
var ownText = function (el) {
var s = "";
for (var i = 0; i < el.childNodes.length; i++) {
if (el.childNodes[i].nodeType === 3) s += el.childNodes[i].nodeValue;
}
return s.trim();
};
all.forEach(function (el) {
var cs = w.getComputedStyle(el);
var txt = ownText(el);
if (!txt) return;
var fs = parseFloat(cs.fontSize);
if (fs > dispMax) { dispMax = fs; dispEl = el.tagName.toLowerCase() + (el.className ? "." + String(el.className).split(/\s+/)[0] : ""); }
if (el.tagName === "P" && (el.textContent || "").trim().length > 40) sizes[fs] = (sizes[fs] || 0) + 1;
});
var bodyPx = 0, best = 0;
Object.keys(sizes).forEach(function (k) { if (sizes[k] > best) { best = sizes[k]; bodyPx = parseFloat(k); } });
if (!bodyPx) bodyPx = parseFloat(w.getComputedStyle(d.body).fontSize);
/* --- 字体家族 --- */
var fams = {};
all.forEach(function (el) {
var ff = w.getComputedStyle(el).fontFamily || "";
var first = ff.split(",")[0].replace(/["']/g, "").trim();
if (first) fams[first] = 1;
});
/* --- 主色数量(背景 + 文字) --- */
var cols = {};
all.forEach(function (el) {
var cs = w.getComputedStyle(el);
var a = rgb2key(cs.backgroundColor); if (a) cols[a] = (cols[a] || 0) + 1;
var b = rgb2key(cs.color); if (b) cols[b] = (cols[b] || 0) + 1;
});
var colN = Object.keys(cols).length;
/* --- 渐变 --- */
var grad = 0;
all.forEach(function (el) {
var bi = w.getComputedStyle(el).backgroundImage || "";
if (/gradient/.test(bi)) grad++;
});
/* --- 章节形状多样性 --- */
var secs = qa("#doc > section, #doc > .band, .release > section", d);
var shapes = {};
secs.forEach(function (s) {
var sig = [];
sig.push(s.querySelector("table") ? "T" : "-");
sig.push(s.querySelector("pre") ? "C" : "-");
sig.push(s.querySelector("svg") ? "V" : "-");
var g = s.querySelector(".grid, .kpis, .claims, .lims, .ledger, .dose, .nm21grid, .rst");
sig.push(g ? (g.className.split(/\s+/).filter(function (x) { return /^g\d|kpis|claims|lims|ledger|dose|nm21grid|rst/.test(x); })[0] || "G") : "-");
sig.push(s.querySelectorAll("h3").length > 1 ? "M" : "-");
shapes[sig.join("")] = 1;
});
/* --- 首屏是否居中标题 + 双按钮 --- */
var first = q("#doc > section, #doc > .band", d) || q("#doc", d) || d.body;
var cs1 = w.getComputedStyle(first);
var centred = cs1.textAlign === "center" ||
!!qa("h1, h2, .h1, .h2", first).filter(function (h) {
return w.getComputedStyle(h).textAlign === "center";
}).length;
var btns = qa("a.btn, button.btn, .linkrow a, .linkrow button", first).length;
/* --- 页面高度 / 视口 --- */
var vhPage = d.documentElement.scrollHeight / VH;
/* --- body 全局 max-width --- */
var bmw = w.getComputedStyle(d.body).maxWidth;
/* --- h1 数量 / 段落宽度 --- */
var h1n = qa("h1", d).length;
var meas = 0, measN = 0;
qa("p", d).forEach(function (p) {
if ((p.textContent || "").trim().length < 60) return;
var cw = p.clientWidth, fs = parseFloat(w.getComputedStyle(p).fontSize);
// 粗略估算一行字符数:中文按 1 个字宽=fs,英文按 0.5
if (cw > 0) { meas += cw / fs; measN++; }
});
var avgCh = measN ? (meas / measN) : 0;
log(slug + ".disp", Math.round(dispMax) + "px " + dispEl);
log(slug + ".body", Math.round(bodyPx) + "px");
log(slug + ".ratio", (dispMax / bodyPx).toFixed(1));
log(slug + ".fam", Object.keys(fams).length + " [" + Object.keys(fams).join("|").slice(0, 60) + "]");
log(slug + ".cols", colN);
log(slug + ".grad", grad);
log(slug + ".secs", secs.length);
log(slug + ".shapes", Object.keys(shapes).length);
log(slug + ".centredHero", centred);
log(slug + ".heroBtns", btns);
log(slug + ".vh", vhPage.toFixed(1));
log(slug + ".bodyMaxW", bmw);
log(slug + ".h1", h1n);
log(slug + ".avgCh", avgCh.toFixed(0));
log(slug + ".midGrey", (function () {
// A6 辅助:有多少「中间尺寸」的文字(16–48px 之间)——中间带扎堆是典型的没做字阶
var mid = 0, tot = 0;
all.forEach(function (el) {
if (el.children.length) return;
if (!(el.textContent || "").trim()) return;
var fs = parseFloat(w.getComputedStyle(el).fontSize);
tot++;
if (fs >= 16 && fs <= 48) mid++;
});
return (100 * mid / (tot || 1)).toFixed(0) + "%";
})());
/* --- 溢出与截断 --- */
var ov = [], clip = [];
all.forEach(function (el) {
var r = el.getBoundingClientRect();
if (r.width && r.height && r.right > VW + 2 &&
!el.closest(".topnav") && !el.closest(".tbl-wrap") && !el.closest(".scrollx") &&
!el.closest(".side") && !el.closest("pre") && !el.closest(".pal") &&
!el.classList.contains("skip")) ov.push(idOf(el));
if (!el.children.length && el.clientWidth > 0 && el.scrollWidth > el.clientWidth + 2 &&
/^(P|H1|H2|H3|TD|SPAN|A|B|I|CODE)$/.test(el.tagName)) {
clip.push(idOf(el) + "[" + (el.textContent || "").slice(0, 16) + "]");
}
});
log(slug + ".ov", d.documentElement.scrollWidth - VW);
log(slug + ".ovEls", ov.slice(0, 3).join("|") || "-");
log(slug + ".clip", clip.length);
log(slug + ".clipEls", clip.slice(0, 3).join("|") || "-");
log(slug + ".undef", ((txt2(d)) .match(/undefined/g) || []).length);
log(slug + ".nan", ((txt2(d)).match(/\bNaN\b/g) || []).length);
/* --- 首屏装置探针 --- */
var cv = d.getElementById("spaceCv");
if (cv) {
log(slug + ".cv", cv.width + "x" + cv.height);
var probe = w.__space;
log(slug + ".probe", probe ? ("steps=" + probe.steps + " t=" + probe.t().toFixed(2) + " prog=" + probe.progress().toFixed(2)) : "MISSING");
try {
var g = cv.getContext("2d");
var rx = Math.round(cv.width * 0.06), ry = Math.round(cv.height * 0.06);
var rw = Math.min(700, cv.width - rx), rh = Math.min(420, cv.height - ry);
var px = g.getImageData(rx, ry, rw, rh).data;
var ink = 0, maxv = 0;
for (var i = 0; i < px.length; i += 4 * 7) {
var v = px[i] + px[i + 1] + px[i + 2];
if (v > 24) ink++;
if (v > maxv) maxv = v;
}
log(slug + ".cvInk", ink + " maxv=" + maxv + " of " + Math.round(px.length / 28));
} catch (e) { log(slug + ".cvInk", "ERR " + e.message); }
log(slug + ".hud0", (q("#hudIdx", d) || {}).textContent + " " + (q("#hudName", d) || {}).textContent);
if (probe && probe.seek) {
probe.seek(0.92); probe.hold();
log(slug + ".hud92", (q("#hudName", d) || {}).textContent + " · " + (q("#hudVal", d) || {}).textContent);
probe.seek(0);
}
log(slug + ".still", d.documentElement.classList.contains("space--still"));
log(slug + ".heroH", Math.round(d.getElementById("hero").getBoundingClientRect().height));
}
}
function txt2(d) { return d.body.innerText || ""; }
function idOf(el) {
if (!el) return "?";
var s = el.tagName.toLowerCase();
if (el.className && typeof el.className === "string") s += "." + el.className.trim().split(/\s+/)[0];
return s;
}
PAGES.forEach(function (u) {
var slug = u.replace(/\W/g, "") || "home";
run(function () { f.src = u; }, 1100);
run(function () { gatePage(slug); }, 300);
});
/* --- B2/B8:滚完一整页之后,不允许还有「没抵达」的元素卡在隐藏态 --- */
run(function () { f.src = "/"; }, 1300);
run(function () {
var d = f.contentDocument, w = f.contentWindow;
var H = d.documentElement.scrollHeight;
var step = Math.max(320, Math.round(w.innerHeight * 0.62));
var y = 0;
(function sweep() {
y += step;
d.documentElement.scrollTop = y;
try { w.scrollTo({ top: y, behavior: "instant" }); } catch (e) { w.scrollTo(0, y); }
try { w.dispatchEvent(new w.Event("scroll")); } catch (e) { w.dispatchEvent(new Event("scroll")); }
if (y < H) setTimeout(sweep, 70);
})();
}, 14000);
run(function () {
var d = f.contentDocument, w = f.contentWindow;
var sel = ".arr-wipe, .arr-num, .arr-cards, .teaser, .reveal";
var all = qa(sel, d);
var off = all.filter(function (e) { return !e.classList.contains("is-in"); });
log("RV_total", all.length);
log("RV_unresolved", off.length);
log("RV_offEls", off.slice(0, 4).map(function (e) { return idOf(e); }).join("|") || "-");
log("RV_arrN", qa(".arr-wipe, .arr-num, .arr-cards", d).length);
log("RV_oldRv", qa(".rv, .rv-group", d).length);
var wip = q(".arr-wipe", d);
if (wip) {
log("RV_wipeClip", w.getComputedStyle(wip).clipPath);
log("RV_wipeH", Math.round(wip.getBoundingClientRect().height));
}
var probe = w.__space;
if (probe) log("RV_spaceT", probe.t().toFixed(2) + " prog=" + probe.progress().toFixed(2));
log("RV_pageH", d.documentElement.scrollHeight);
/* 虚拟时间下过渡不推进,读到的永远是起始值。
关掉过渡再读一次,验的是「CSS 目标值」而不是「过渡当前值」。 */
var st = d.createElement("style");
st.textContent = "*,*::before,*::after{transition:none !important}";
d.head.appendChild(st);
var wip2 = q(".arr-wipe", d);
if (wip2) log("RV_wipeTarget", w.getComputedStyle(wip2).clipPath);
var num2 = q(".arr-num", d);
if (num2) log("RV_numTarget", w.getComputedStyle(num2).opacity + " / " + w.getComputedStyle(num2).transform);
var card2 = q(".arr-cards > *", d);
if (card2) log("RV_cardTarget", w.getComputedStyle(card2).opacity + " / " + w.getComputedStyle(card2).transform);
var kick = q(".band__k + .h2", d);
if (kick) log("RV_bandHead", Math.round(parseFloat(w.getComputedStyle(kick).fontSize)) + "px");
}, 500);
next();
</script>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从磁盘上的真实评测 JSON 生成站点用的台账数据 assets/js/ledger.js。
⚠️ 生成物不要手改;改数据请改上游 JSON 后重跑本脚本。
来源(两份都是工程里保存的正式运行结果,逐字复制,不做任何改写或换算):
A. H:\\Memory\\V2_dpskw\\runtime_score_bands.json 60 episodes
B. H:\\Memory\\V2_dpskw\\nm2_1_final_runtime_e2e.json 48 episodes
用法:
python tools/make_ledger.py
"""
from __future__ import annotations
import io
import json
import os
SOURCES = [
{
"key": "bands",
"label": "运行时评分带扫描",
"path": r"H:\Memory\V2_dpskw\runtime_score_bands.json",
"config": "0.00",
"note": "60 用例 · 24 同形候选 · prior=1.00 blend=0.00",
},
{
"key": "e2e",
"label": "NM2.1 最终运行时",
"path": r"H:\Memory\V2_dpskw\nm2_1_final_runtime_e2e.json",
"config": "prior=1.00 blend=0.50",
"note": "48 用例 · 24 同形候选 · prior=1.00 blend=0.50",
},
]
OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"assets", "js", "ledger.js")
def rows_of(path: str, key: str):
with open(path, encoding="utf-8") as fh:
j = json.load(fh)
rows = j.get("rows") or {}
if key not in rows:
raise SystemExit("在 %s 里找不到 rows[%r]" % (path, key))
return rows[key]
def main() -> int:
out, total = [], 0
for src in SOURCES:
rows = rows_of(src["path"], src["config"])
total += len(rows)
out.append({
"key": src["key"],
"label": src["label"],
"note": src["note"],
"n": len(rows),
"items": [{
"id": r.get("id"),
"attr": r.get("attribute"),
"ans": bool(r.get("answerable")),
"exp": r.get("expected"),
"reply": r.get("reply"),
"ok": bool(r.get("correct")),
"wrong": bool(r.get("wrong_attribute")),
"leak": bool(r.get("leaked")),
"stop": r.get("stop_reason"),
"sel": r.get("records_selected"),
"score": r.get("top_score"),
"active": r.get("records_active"),
"need": r.get("need_memory"),
} for r in rows],
})
body = io.StringIO()
body.write("/* ==================================================================\n")
body.write(" Natural Memory v2 — ledger.js\n")
body.write(" 「真实评测台账」的数据层。\n\n")
body.write(" ⚠️ 本文件由 tools/make_ledger.py 从磁盘上的原始评测 JSON 生成,\n")
body.write(" 请勿手改。下面是全部 %d 条 episode,字段逐字复制、未做改写或换算。\n\n" % total)
for s in SOURCES:
body.write(" %s\n" % os.path.basename(s["path"]))
body.write(" ================================================================== */\n")
body.write("window.NM_LEDGER = ")
body.write(json.dumps(out, ensure_ascii=False, separators=(",", ":"), indent=0)
.replace("\n", ""))
body.write(";\n")
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8", newline="\n") as fh:
fh.write(body.getvalue())
print("写出 %s(%d 条 episode / %d 字节)" % (OUT, total, os.path.getsize(OUT)))
return 0
if __name__ == "__main__":
raise SystemExit(main())