109 lines
4.2 KiB
Python
109 lines
4.2 KiB
Python
# pyright: reportMissingImports=false, reportAttributeAccessIssue=false
|
||
# 说明:LSP 的 Python 环境看不到本项目包(未安装进它的 site-packages),
|
||
# 会把「包内互相 import」误报为缺失模块。运行时导入已由执行验证通过。
|
||
|
||
"""生成迷你年表与人物索引(随成品入库的两种辅助文件)。"""
|
||
from __future__ import annotations
|
||
|
||
from collections import Counter
|
||
from pathlib import Path
|
||
|
||
from dfannals import legends as lg
|
||
from dfannals import score
|
||
from dfannals.legends import Event, World
|
||
|
||
# 评分标准统一在 dfannals/score.py,年表与切章共用同一套。
|
||
significance = score.significance
|
||
category = score.category
|
||
|
||
|
||
def event_line(world: World, e: Event) -> str:
|
||
"""把一条事件压成一行可读文字。"""
|
||
chunks: list[str] = [e.type]
|
||
for tag, kind in lg.ID_FIELDS.items():
|
||
for i in e.ids(tag):
|
||
if kind == "figure":
|
||
name = world.figure_name(i)
|
||
elif kind == "site":
|
||
name = world.site_name(i)
|
||
elif kind == "entity":
|
||
name = world.entity_name(i)
|
||
else:
|
||
name = ""
|
||
if name:
|
||
chunks.append(f"{tag}={name}")
|
||
return f"- **{e.year}** [{category(e)}] " + " · ".join(chunks)
|
||
|
||
|
||
def build_timeline(world: World, min_score: int = 70, per_decade: int = 14) -> str:
|
||
"""按十年分组的重要事件年表。"""
|
||
prom = score.prominence(world)
|
||
lo, hi = world.event_years()
|
||
important = [e for e in world.events if significance(e, prom) >= min_score]
|
||
buckets: dict[int, list[Event]] = {}
|
||
for e in important:
|
||
buckets.setdefault((e.year // 10) * 10, []).append(e)
|
||
|
||
lines = [
|
||
f"# {world.name} · 年表",
|
||
"",
|
||
f"- 历史跨度:{lo}–{hi} 年" if hi else "- 历史跨度:未知",
|
||
f"- 事件总数:{len(world.events)},其中重要事件 {len(important)}",
|
||
f"- 历史人物 {len(world.figures)} 位 · 文明与组织 {len(world.entities)} 个 · 地点 {len(world.sites)} 处",
|
||
"",
|
||
"> 本表由 legends 导出数据自动生成,只收录叙事权重较高的事件;"
|
||
"完整数据留在本地,不入库。",
|
||
"",
|
||
]
|
||
for decade in sorted(buckets):
|
||
evs = sorted(buckets[decade], key=lambda e: (e.year, e.seconds72, e.id))
|
||
shown = sorted(evs, key=lambda e: -significance(e, prom))[:per_decade]
|
||
shown.sort(key=lambda e: (e.year, e.seconds72, e.id))
|
||
lines.append(f"## {decade}s")
|
||
lines.append("")
|
||
lines.extend(event_line(world, e) for e in shown)
|
||
if len(evs) > len(shown):
|
||
lines.append(f"- …另有 {len(evs) - len(shown)} 条次要事件未列")
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def build_figures(world: World, top: int = 120) -> str:
|
||
"""人物索引:按参与事件数排序,给出身份与生卒。"""
|
||
involvement: Counter[int] = Counter()
|
||
for e in world.events:
|
||
seen: set[int] = set()
|
||
for tag, kind in lg.ID_FIELDS.items():
|
||
if kind != "figure":
|
||
continue
|
||
for i in e.ids(tag):
|
||
seen.add(i)
|
||
involvement.update(seen)
|
||
|
||
lines = [
|
||
f"# {world.name} · 人物索引",
|
||
"",
|
||
f"共 {len(world.figures)} 位历史人物,按在史料中出现的次数排序(前 {top} 位)。",
|
||
"",
|
||
"| 人物 | 种族 | 生卒 | 职业 | 事件数 |",
|
||
"|---|---|---|---|---|",
|
||
]
|
||
ranked = sorted(world.figures.values(), key=lambda f: -involvement.get(f.id, 0))
|
||
for fig in ranked[:top]:
|
||
name = lg.pretty(fig.name) or f"HF#{fig.id}"
|
||
prof = fig.profession or "—"
|
||
lines.append(
|
||
f"| {name} | {fig.race or '—'} | {fig.alive_span} | {prof} | {involvement.get(fig.id, 0)} |"
|
||
)
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def write_outputs(world: World, out_dir: Path, min_score: int = 40) -> list[Path]:
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
timeline = out_dir / "timeline.md"
|
||
figures = out_dir / "figures.md"
|
||
timeline.write_text(build_timeline(world, min_score=min_score), encoding="utf-8")
|
||
figures.write_text(build_figures(world), encoding="utf-8")
|
||
return [timeline, figures]
|