主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine 由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
185 lines
6.9 KiB
Python
185 lines
6.9 KiB
Python
"""把史料切成章节素材。
|
||
|
||
真实数据(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:
|
||
# render_refs 会带出双方(比如 slayer_hfid=谁),而不是只剩单方面 hfid
|
||
chunks = [f"{e.year}年", e.type, *world.render_refs(e)]
|
||
for i in e.figure_ids():
|
||
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)
|