- 新增 dfannals/glossary.py(译名表)与 dfannals/localize.py(渲染层) 人名=音译+绰号意译(Ral Fastenhatchets→拉尔·缚斧、朗古德·商熊「剖胎之寒」), 地名整体意译(Fordedwinds→涉风渡、The Volcano Of Ravens→渡鸦火山) - 生成链路本土化:素材渲染、人物表、扩写白名单全部走译名;已有产物做确定性替换 - 修复 factcheck 的 \b 盲点:Unicode 下汉字也算词字符,紧贴汉字的英文名不被检查 (曾因此漏掉编造专名)。修好后抓出并定点修掉长版第 1 集的 2 处编造 - 修正 title_form 大小写(YETI→Yeti),此前 Yeti/Goblin 一直漏译 - 新增 tests/test_localize.py(13 项回归)
矮人要塞人物连载 · 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/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 所有。本仓库只包含由其生成数据的衍生文本, 不含游戏本体、美术资源或存档。