规则库 3.8.1 + 排盘修复 + 评测体系

- 规则库 3.6→3.8.1(542→548 条):剪枝上锁、R-杂-38 从格临界双轨、
  R-事-10~13 冲刑方向判别、R-禁-23 自造必要条件之禁
- 排盘修复随 3.6.2 已发:港澳时区夏令时双向误拨、晚子时双重换日
- 新增 评测/ 目录(BaziQA 题答分离、判分工具、记分卡、with/without-skill 对照)
  与 scripts/query_rules.py 条款检索;ZIPEC_EVAL 评测额度豁免
- manifest 重建并重新签名;VERSION 保持 3.6.2,不发版不打 tag
This commit is contained in:
2026-10-13 12:34:36 +08:00
parent b20fd5c3c9
commit 1a0eed1999
338 changed files with 36533 additions and 30 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""得分.json → 单文件 HTML 记分卡(模板 scorecard_template.html)。
沿用技能盘面图的品牌风格:logo 与品牌字体从母版 scripts/render/assets/
以 base64 内嵌(单文件便携),亮色卡片风。数据全部由 得分.json 注入,
模板不含任何硬编码分数。
得分.json 字段 schema(收录新模型时按此填写):
model, skill_mode(with-skill / without-skill), skill_version, eval_date,
benchmark,
overall: {total, correct, accuracy, no_answer}
baseline: {name, overall: {total, correct, accuracy},
per_contest: {contest: accuracy}, per_dimension: {维度: accuracy}}
per_contest: {contest: {total, correct, accuracy, no_answer}}
per_dimension:{维度: {total, correct, accuracy, no_answer}}
用法:
python render_scorecard.py [--score <得分.json>] [--out <输出.html>]
[--template <模板>] [--assets <render/assets>]
默认:--score 评测/ai模型纬度得分/kimi-k3/with-skill/得分.json,
输出与得分.json 同目录的 得分.html。
"""
from __future__ import annotations
import argparse
import base64
import json
import sys
from html import escape
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] # 评测/
SKILL = Path(__file__).resolve().parents[2] # 技能母版根
DEFAULT_SCORE = ROOT / "ai模型纬度得分" / "kimi-k3" / "with-skill" / "得分.json"
DIM_ORDER = ["流年运势", "感情", "六亲", "事业", "性格", "学业", "财富", "健康", "其他"]
# 结构性占位符:值是装配层渲染的 HTML 片段或 base64 资源,填充时原样注入;
# 其余占位符均为文本字段,一律 HTML 转义防注入(与盘面图装配层同口径)。
STRUCTURAL = {"LOGO_B64", "FONT_B64", "DIM_BARS", "CONTEST_ROWS"}
def asset_b64(assets: Path, name: str) -> str:
f = assets / name
return base64.b64encode(f.read_bytes()).decode("ascii") if f.is_file() else ""
def dim_bars(score: dict) -> str:
rows = []
dims = score["per_dimension"]
base = score["baseline"].get("per_dimension", {})
for d in DIM_ORDER + sorted(set(dims) - set(DIM_ORDER)):
if d not in dims:
continue
s = dims[d]
b = base.get(d)
name = escape(d)
ours = f'<div class="bar ours" style="width:{s["accuracy"]}%"></div>'
ours += (f'<div class="bar base" style="width:{b}%"></div>'
if isinstance(b, (int, float)) else "")
val = (f'<b>{s["accuracy"]}%</b>({s["correct"]}/{s["total"]})'
+ (f' <span style="color:var(--muted)">基线 {b}%</span>'
if isinstance(b, (int, float)) else ""))
rows.append(f'<div class="dimrow"><div class="dimname">{name}</div>'
f'<div class="dimtrack">{ours}</div>'
f'<div class="dimval">{val}</div></div>')
return "\n ".join(rows)
def contest_rows(score: dict) -> str:
rows = []
base = score["baseline"].get("per_contest", {})
for c in sorted(score["per_contest"]):
s = score["per_contest"][c]
b = base.get(c)
if isinstance(b, (int, float)):
diff = round(s["accuracy"] - b, 1)
cls = "up" if diff >= 0 else "down"
delta = (f'<span class="delta {cls}">{"+" if diff >= 0 else ""}'
f'{diff}pp</span>')
btxt = f"{b}%"
else:
delta, btxt = "—", "—"
rows.append(
f'<tr><td class="dim">{escape(c)}</td>'
f'<td><b>{s["accuracy"]}%</b>({s["correct"]}/{s["total"]},'
f'未答 {s.get("no_answer", 0)})</td>'
f'<td>{btxt}</td><td>{delta}</td></tr>')
return "\n ".join(rows)
def render(score: dict, template: str, assets: Path, score_json_name: str) -> str:
ov = score["overall"]
bo = score["baseline"]["overall"]
delta = round(ov["accuracy"] - bo["accuracy"], 1)
values = {
"TITLE": f"评测记分卡:{score['model']}({score['skill_mode']})",
"LOGO_B64": asset_b64(assets, "lingyao-logo.png"),
"FONT_B64": asset_b64(assets, "logo-font.ttf"),
"MODEL": score["model"],
"SKILL_MODE_LABEL": "加载技能" if score["skill_mode"] == "with-skill"
else "裸答对照",
"BENCHMARK": score["benchmark"],
"SKILL_VERSION": score.get("skill_version", "—"),
"EVAL_DATE": score.get("eval_date", "—"),
"OVERALL_ACC": f'{ov["accuracy"]}',
"OVERALL_CORRECT": f'{ov["correct"]}',
"OVERALL_TOTAL": f'{ov["total"]}',
"NO_ANSWER": f'{ov.get("no_answer", 0)}',
"BASELINE_NAME": score["baseline"].get("name", "官方基线"),
"BASELINE_ACC": f'{bo["accuracy"]}',
"DELTA_CLASS": "up" if delta >= 0 else "down",
"DELTA_TEXT": f'({"+" if delta >= 0 else ""}{delta}pp)',
"DIM_BARS": dim_bars(score),
"CONTEST_ROWS": contest_rows(score),
"SCORE_JSON": score_json_name,
}
out = template
for k, v in values.items():
out = out.replace("{{" + k + "}}", v if k in STRUCTURAL else escape(str(v)))
leftover = [p.split("}}")[0] for p in out.split("{{")[1:]]
assert not leftover, f"模板存在未填充占位符: {leftover}"
return out
def main() -> int:
ap = argparse.ArgumentParser(description="得分.json → 单文件 HTML 记分卡")
ap.add_argument("--score", type=Path, default=DEFAULT_SCORE)
ap.add_argument("--out", type=Path, default=None,
help="默认与得分.json 同目录的 得分.html")
ap.add_argument("--template", type=Path,
default=Path(__file__).resolve().parent / "scorecard_template.html")
ap.add_argument("--assets", type=Path,
default=SKILL / "scripts" / "render" / "assets")
a = ap.parse_args()
score = json.loads(a.score.read_text(encoding="utf-8"))
template = a.template.read_text(encoding="utf-8")
out = a.out or a.score.with_name("得分.html")
html = render(score, template, a.assets, a.score.name)
out.write_text(html, encoding="utf-8")
print(f"OK: {out}({len(html)} 字符,logo/字体"
f"{'已内嵌' if asset_b64(a.assets, 'lingyao-logo.png') else '未找到,已留空'})")
return 0
if __name__ == "__main__":
sys.exit(main())