Files
dwarf-fortress-annals/dfannals/slice.py
T
Chen Yi e0073e90bd 第 1 卷(人物主线):第 1–3 集 + 人物表
主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine
由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
2026-10-05 19:28:49 +08:00

185 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""把史料切成章节素材。
真实数据(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)