第 1 卷第 1 章:1–9 年
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user