第 1 卷第 1 章:1–9 年

This commit is contained in:
Chen Yi
2026-10-05 17:30:11 +08:00
parent c56394afdd
commit 3468433c9a
34 changed files with 2564 additions and 4 deletions
+195
View File
@@ -0,0 +1,195 @@
"""把史料切成章节素材。
真实数据(403853 条事件 / 250 年)证明:按"重要事件数量"切章会得到几千章。
所以改为先按时间窗分桶(每窗 10 年),再在窗内按显著度取前 N 条——
这样一章天然覆盖一段年月,既有大事件链,也不会漏掉时代的推进。
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from dfannals import legends as lg
from dfannals import score
from dfannals.legends import Event, World
YEARS_PER_CHAPTER = 10 # 一个时间窗覆盖的年数
EVENTS_PER_CHAPTER = 12 # 每章最多收录的史料条目
MIN_EVENTS_PER_CHAPTER = 3 # 少于这个数量就并入上一章(太平年月)
MAX_PER_CATEGORY = 4 # 同一类事件每章上限,避免整章都是讣告
@dataclass
class Chapter:
index: int
start_year: int
end_year: int
events: list[Event] = field(default_factory=list)
quiet_years: list[tuple[int, int]] = field(default_factory=list)
@property
def key(self) -> str:
return f"{self.index:03d}-{self.start_year}-{self.end_year}"
@property
def span(self) -> str:
if self.start_year == self.end_year:
return f"{self.start_year} 年"
return f"{self.start_year}–{self.end_year} 年"
def categories(self) -> list[str]:
seen: list[str] = []
for e in self.events:
c = score.category(e)
if c not in seen:
seen.append(c)
return seen
def plan(
world: World,
years_per_chapter: int = YEARS_PER_CHAPTER,
per_chapter: int = EVENTS_PER_CHAPTER,
min_events: int = MIN_EVENTS_PER_CHAPTER,
max_per_category: int = MAX_PER_CATEGORY,
) -> list[Chapter]:
prom = score.prominence(world)
scored = [(score.significance(e, prom), e) for e in world.events]
scored = [pair for pair in scored if pair[0] > 0]
if not scored:
lo, hi = world.event_years()
return [Chapter(index=1, start_year=lo, end_year=hi)]
lo = min(e.year for _, e in scored)
hi = max(e.year for _, e in scored)
buckets: dict[int, list[tuple[int, Event]]] = defaultdict(list)
for sig, e in scored:
buckets[e.year // years_per_chapter].append((sig, e))
chapters: list[Chapter] = []
for b in range(lo // years_per_chapter, hi // years_per_chapter + 1):
window_start = max(b * years_per_chapter, lo)
window_end = min(b * years_per_chapter + years_per_chapter - 1, hi)
ranked = sorted(buckets.get(b, []), key=lambda p: (-p[0], p[1].year, p[1].id))
# 按类别限额挑选:让一章里有战事、兴亡、器物,而不是 12 条死亡讣告
picks: list[tuple[int, Event]] = []
used: dict[str, int] = {}
for sig, ev in ranked:
cat = score.category(ev)
if used.get(cat, 0) >= max_per_category:
continue
used[cat] = used.get(cat, 0) + 1
picks.append((sig, ev))
if len(picks) >= per_chapter:
break
events = sorted((e for _, e in picks), key=lambda e: (e.year, e.seconds72, e.id))
if not events:
if chapters:
chapters[-1].quiet_years.append((window_start, window_end))
chapters[-1].end_year = window_end
continue
if len(events) < min_events and chapters:
chapters[-1].events.extend(events)
chapters[-1].end_year = window_end
continue
chapters.append(
Chapter(index=len(chapters) + 1, start_year=window_start, end_year=window_end, events=events)
)
for i, c in enumerate(chapters, 1):
c.index = i
return chapters
def _figure_line(world: World, fid: int, prom) -> str:
fig = world.figures.get(fid)
if not fig:
return f"- HF#{fid}(史料无记载)"
bits = [lg.pretty(fig.name) or f"HF#{fid}", fig.race or "未知种族", fig.alive_span]
if fig.profession:
bits.append(fig.profession)
bits.append(f"史料出现 {prom.get(fid, 0)} 次")
return "- " + " | ".join(bits)
def material(world: World, chapter: Chapter, max_figures: int = 40) -> str:
"""把一章的史料渲染成给模型看的素材文本。"""
prom = score.prominence(world)
lines: list[str] = [
f"## 本章范围:{chapter.span}",
"",
f"共 {len(chapter.events)} 条史料,类型:{('、'.join(chapter.categories())) or '无'}。",
"",
"### 事件流水(按时间排序,专名保留游戏原文)",
"",
]
participation: dict[int, int] = {}
for e in chapter.events:
chunks = [f"{e.year}年", e.type]
for tag, kind in lg.ID_FIELDS.items():
for i in e.ids(tag):
# id 为负是“无此项”,不要渲染成 HF#-1 这类噪音
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 not name:
continue
chunks.append(f"{tag}={name}")
if kind == "figure":
participation[i] = participation.get(i, 0) + 1
extra = []
for field_name in ("state", "reason", "circumstance"):
v = e.text(field_name)
if v and v not in ("-1", ""):
extra.append(f"{field_name}={v}")
if extra:
chunks.append("·".join(extra))
lines.append("- " + " · ".join(chunks))
if chapter.quiet_years:
spans = "、".join(f"{a}–{b} 年" for a, b in chapter.quiet_years)
lines += [
"",
"### 太平年月(史料无值得立传的事件)",
"",
f"- {spans}:这段时期只留下琐碎记录,可一笔带过。",
]
lines += ["", "### 本章登场人物(史料中的身份信息)", ""]
for fid, _ in sorted(participation.items(), key=lambda kv: -prom.get(kv[0], 0))[:max_figures]:
lines.append(_figure_line(world, fid, prom))
sites = sorted({n for e in chapter.events for i in e.ids("site_id") if (n := world.site_name(i))})
if sites:
lines += ["", "### 相关地点", "", "- " + "、".join(sites)]
ents = sorted({n for e in chapter.events for i in e.ids("entity_id") if (n := world.entity_name(i))})
if ents:
lines += ["", "### 相关势力", "", "- " + "、".join(ents)]
return "\n".join(lines)
def overview(world: World, chapters: list[Chapter], limit: int = 10) -> str:
lo, hi = world.event_years()
lines = [
f"世界:{world.name}" + (f"({world.altname})" if world.altname else ""),
f"历史:{lo}–{hi} 年 | 史料事件 {len(world.events)} 条 | "
f"人物 {len(world.figures)} | 势力 {len(world.entities)} | 地点 {len(world.sites)}",
f"规划章节:{len(chapters)} 章(每章约 {YEARS_PER_CHAPTER} 年 / 最多 {EVENTS_PER_CHAPTER} 条史料)",
]
for c in chapters[:limit]:
lines.append(f" 第 {c.index} 章 {c.span}:{len(c.events)} 条({('、'.join(c.categories())) or '无'})")
if len(chapters) > limit:
lines.append(f" …… 其余 {len(chapters) - limit} 章略")
return "\n".join(lines)