Files
dwarf-fortress-annals/README.md
T
Chen Yi 8630dcad55 文笔层:AI 味机检、标点规范化、分块大量扩写(第 1 集成稿 10805 字)
- 修复上次提交把 dfannals/cli.py 写成 0 字节的问题(它是唯一入口,导致管道不可运行)
- 性别:解析 <caste>,人物表与写作素材带性别(Ral Fastenhatchets 实为女性)
- 新增 dfannals/deslop.py:AI 味机械诊断(硬伤词/句式/标点,按千字密度报告)
- 新增 dfannals/normalize.py:标点与结构清理(引号配对、重复段落与句子、模型自加的小节标记)
- 新增 dfannals/expand.py 与 prompts/literary-expander.md:按年份场景分块大量扩写
- 专名防幻觉:每块附史料专名白名单,事后按段自动修复可疑专名
- episode 命令并进文笔层(骨架稿另存 .skeleton.md),新增 expand 命令做 A/B 对照
- 新增 notes/switched-threads.md 与 test_deslop / test_normalize 回归测试
- 提交前拦截「已跟踪文件为空」,防止上述事故复发
2026-10-05 22:27:02 +08:00

10 KiB
Raw Blame History

矮人要塞人物连载 · Dwarf Fortress Annals

把《矮人要塞》(Dwarf Fortress) 世界生成的历史数据,变成跟着人走的中文连载故事,自动推送到这个仓库。

不是编年史。编年史的主语是年代,读起来必然像流水账(本项目第一版就是这样被否掉的)。 这里的主语是人:先从史料里自动挖出一组关系密切、反复交锋的角色,再按因果链切集, 以"这些人物的传记作者"的视角贴着他们写。


一、它怎么工作

① 线索挖掘   从 40 万条事件里建人物关系图,找出关系密切、有戏的 3–6 人小圈子
             (只用有叙事含义的互动:对战/结缘/反目/绑架/构陷/迫害……)
② 切集       沿这条线按因果链切集:相邻事件间隔 >2 年断开,短链合并,过长拆上下集
③ 人物表     表格 + 每人一两句小传;"身份"由事件字段反推,不是猜的
④ 写作       主角的传记作者视角:他们的目标、算计、得失做主语;
             世界大事只在影响到主角时提及
⑤ 自评闸门   5 个维度各 0–5 分,总分 <12 判"流水账" → 先重写一次 →
             仍不合格就换下一条线索,并把放弃原因写进 notes/
⑥ 发布       git commit & push 到本仓库(原始史料与存档不入库)

二、这一卷的主角

世界 Mon Sagus(The Plane of Dawn),Medium 129×129 / 250 年历史
主角组 Guspu Frillyknots(鹰人掮客)、Ral Fastenhatchets(矮人)、Alath Blottedmine(矮人)
线索 46 年间一个掮客反复收买、构陷一位管货栈的矮人,屡屡失手
选线依据 挖掘器排序第一名:有效互动 11 次、转折 11 次、阴谋×8 + 构陷×3

详见 output/volumes/v1/cast.md。

三、目录

路径 内容 入库
output/volumes/v<N>/chapters/ 连载正文 ✅
output/volumes/v<N>/cast.md 人物表(表格 + 小传) ✅
output/volumes/v<N>/timeline.md 迷你年表 ✅
output/volumes/v<N>/figures.md 人物索引(按史料出场次数排序) ✅
notes/switched-threads.md 被放弃的线索及原因 ✅
dfannals/ 管道源码 ✅
prompts/biographer.md 写作规范(决定视角、边界、篇幅) ✅
scripts/ 安装、部署、驱动、启动器 ✅
tests/ 回归测试(解析、切集、闸门、事实校验) ✅
data/exports/ 原始 legends 数据与预处理缓存 ❌
data/state.json、data/threads.json、data/selection.json 进度、候选、选线依据 ❌

四、依赖

依赖 版本 / 说明
Dwarf Fortress 53.16 免费版(Bay 12),必须与 DFHack 严格配对
DFHack 53.16-r2,Windows 版经 dfhooks.dll 注入,不必替换启动器
Python 3.12
defusedxml sudo apt install -y python3-defusedxml
写作/自评模型 经 https://wb2api.hajim1.art/v1(OpenAI 兼容)调用 cn:deepseek-v4-pro
发布 现有 ~/.ssh/id_ed25519(已注册到 Gitea 账号 gitadmin)

启动器:一律用 scripts/dfannals ...,不要直接 python3 -m dfannals.cli。 会话 PATH 上的 python3 可能是编辑器工具链自建的 venv(没有 defusedxml, 且关闭了 user site),启动器会自动挑一个能 import defusedxml 的解释器。

五、安装

scripts/install-df.sh --download     # 装 DF + DFHack(Bay12 慢,会断点续传)
sudo apt install -y python3-defusedxml
scripts/deploy-dfhack.sh /mnt/c/Users/$USER_WIN/DwarfFortress   # 部署观测/驱动脚本

六、产出一卷

# 1) 生成新世界并导出 legends(详见第九节)
scripts/make-world.sh 3 3            # 3 = Medium 大小,3 = 250 年

# 2) 建索引:年表 + 人物索引
scripts/dfannals index

# 3) 看候选线索(含信号明细),挑一条
scripts/dfannals threads --top 8 --samples 2

# 4) 看这条线切出的剧集与预算
scripts/dfannals episodes --thread 1

# 5) 生成人物表
scripts/dfannals cast --thread 1

# 6) 逐集产出(自动过闸门;不合格自动重写,再不合格自动换线索)
scripts/dfannals episode --index 1
scripts/dfannals episode --index 2
scripts/dfannals episode --index 3

实测:单集约 2–4 分钟(写作 1 次 + 自评 1 次,若判平淡会多一次重写),篇幅 800–1500 字。

七、配置在哪

想改什么 改哪
哪些互动算"关系"、权重、转折标记 dfannals/threads.py 的 SIGNALS
主线口径(≥5 次互动、3–6 人、跨度 ≥30 年) dfannals/threads.py 顶部常量
切集参数(间隔 >2 年断开、每集预算) dfannals/episodes.py 顶部常量
自评维度、阈值 12、重写/换线上限 dfannals/gate.py 的 DIMENSIONS / THRESHOLD / MAX_REWRITES / MAX_CANDIDATES
视角、文风、演绎边界、篇幅 prompts/biographer.md
同人同类事件的重复上限 dfannals/volume.py 的 PER_KEY_CAP
模型与网关 dfannals/config.py 的 PROVIDER / MODEL
发布目标、仓库名 dfannals/config.py 的 GITEA_*

API key 不入库:运行时从 ~/.pi/agent/auth.json 读取,只在内存中,不打印、不落盘、不进 git。

八、子命令

scripts/dfannals status                 当前世界 / 卷号 / 已完成集数
scripts/dfannals index                  解析 exports → 年表 + 人物索引
scripts/dfannals threads                候选线索与信号明细
scripts/dfannals episodes --thread N    某条线索的剧集规划与预算
scripts/dfannals cast --thread N        人物表(--no-bios 只出表格)
scripts/dfannals episode --index N      产出第 N 集(--dry-run 不落盘)

测试:/usr/bin/python3 tests/test_factcheck.py、test_episodes.py、test_gate.py。

九、世界是怎么造出来的

scripts/make-world.sh 全程从外部驱动游戏,不需要人工点击:

  • dfannals/screentext 把画面读成字符网格,scripts/df-screen.py 按文字定位按钮
  • dfannals/click 模拟鼠标,dfannals/setparams 直接改内存里的世界生成参数
  • dfannals/legends 进入 legends 模式(做法取自 DFHack 官方 open-legends.lua)
  • 导出 Export XML(原生 legends.xml)与 exportlegends(扩展数据)

备选:"Dwarf Fortress.exe" -gen 1 RANDOM "MEDIUM ISLAND" 可静默生成并导出, 但不留存档,无法继续进 legends,只适合快速验证参数。

十、踩过的坑(失败排查参考)

症状 原因与解法
not well-formed (invalid token) DF 在名字里嵌了 \x10/\x11 等 CP437 控制字符,XML 1.0 禁止。预处理会剔除并转 UTF-8,缓存到 data/exports/clean/
事件数莫名翻倍 预处理缓存若与原始文件同目录,会被 *.xml 扫描当成第二份史料。缓存因此放在子目录
素材里出现 HF#-1、Site#-1 DF 用负数占位表示"无此项"。查询接口对负 id 返回空串,渲染时跳过
人物关系在素材里丢失 真实史料用 hfid_target/group_1_hfid/snatcher_hfid/seeker_hfid/hfid1… 共 40+ 个字段表达关系;只认 hfid 会把"谁对谁做了什么"丢掉。现按"字段名含 hfid 即为人物"分类
校验器把真名报成可疑 史料里是全小写程序名(yemi deermoths),正文做了首字母大写。已知名字集同时收录两种形式(有回归测试)
候选线索全是"同一对人反复对战" 裸互动次数会被机械重复刷满(第一名 49 次里有 35 次是"反复请求拜师被拒")。现剔除重复性请求,并对同人同类事件封顶、要求 ≥2 种信号
主角组里混进泰坦巨兽 用"文明种族白名单(五族 + *_MAN)",实测覆盖 96.5% 的历史人物
一集被均分成两个 770 字的碎片 均分只看条数、不看预算。改为先取最小段数,再确保每段不低于下限
自评/小传返回空正文 推理模型的思考与正文共用 max_tokens,喂长材料时会烧光预算。结构化输出统一用 MAX_TOKENS_STRUCTURED
ModuleNotFoundError: defusedxml PATH 上的 python3 被编辑器工具链的 venv 抢走了。用 scripts/dfannals 启动器
dfhack-run "cmd a b" 报 not recognized 参数要分开传:dfhack-run dfannals/click 10 2,不要整体加引号
Bay12 下载只有 ~10KB/s;GitHub 直连 0 字节 官网无国内镜像,放后台断点续传;GitHub 用 https://gh-proxy.com/ 前缀
自定义 init 脚本不生效 53.16 只执行固定几个 init 文件。改用 dfhack-run 从外部驱动更可靠

十一、闸门到底能不能判出流水账

scripts/gate-selfcheck.py 是这个闸门的校准证据(跑两次真实自评):

样本 总分 判定
故意构造的平淡稿(纯事件罗列) 0/25 判平淡 ✓
场景级有戏剧本 16/25 通过 ✓

阈值 12 在两者之间,区分有效。注意:阈值是校准出来的,不是拍出来的—— 第一版用"梗概体"当好稿样本时只得 11 分,换了场景级样本才验证出尺度没偏严。

十二、当前状态与限制

  • 已完成:第 1 卷 3 集上线 + 人物表 + 年表;旧的两章流水账已用普通提交删除(历史保留)
  • 定时任务未启用:cron / 开机自启等稳定运行一段时间后再落地,目前按需手动执行
  • 原始数据不入库:data/ 已在 .gitignore 中
  • 已知局限:
    • 关系图的边来自成对事件,独狼型人物(独自创作、被自然杀死)进不了线索挖掘
    • 多参与者事件(比赛、集体迫害)会把同场者两两连边,圈子的"关系"偏共处而非互动
    • 事实校验只拦得住凭空专名;动机、心理、对白全是允许的演绎,没有自动防线

十三、声明

《Dwarf Fortress》版权归 Bay 12 Games 所有。本仓库只包含由其生成数据的衍生文本, 不含游戏本体、美术资源或存档。