Files
dwarf-fortress-annals/README.md
T
Chen Yi 70ce758e8d 第 1 卷三集全部转为成稿(分块扩写 + AI 腔定点返修),文档同步
- 每集两稿:NNN-….md 为成稿(9492/11260/11869 字),NNN-….skeleton.md 为过闸门的骨架稿
- 新增 expand.repair_style:只重写含 AI 腔标记的段落(本例出现了 14 处破折号),
  三集硬伤密度 1.2/1.0/1.5 → 0.1/0.3/0.0(每千字)
- episode 命令接入定点返修;expand 命令保留 A/B 对照用法
- README 同步文笔层、本土化、子命令、以及本轮新踩的坑(\b 盲点、枚举大小写、自制小节标记)
- 第 3 集骨架连换三条线索仍未过闸门(真实拒绝),其成稿基于既有骨架扩写
2026-10-05 23:14:24 +08:00

224 lines
14 KiB
Markdown
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.
# 矮人要塞人物连载 · Dwarf Fortress Annals
把《矮人要塞》(Dwarf Fortress) 世界生成的历史数据,变成**跟着人走的中文连载故事**,自动推送到这个仓库。
不是编年史。编年史的主语是年代,读起来必然像流水账(本项目第一版就是这样被否掉的)。
这里的主语是**人**:先从史料里自动挖出一组关系密切、反复交锋的角色,再按因果链切集,
以"这些人物的传记作者"的视角贴着他们写。
---
## 一、它怎么工作
```text
① 线索挖掘 从 40 万条事件里建人物关系图,找出关系密切、有戏的 3–6 人小圈子
(只用有叙事含义的互动:对战/结缘/反目/绑架/构陷/迫害……)
② 切集 沿这条线按因果链切集:相邻事件间隔 >2 年断开,短链合并,过长拆上下集
③ 人物表 表格 + 每人一两句小传;"身份"由事件字段反推,不是猜的
④ 写作 主角的传记作者视角:他们的目标、算计、得失做主语;
世界大事只在影响到主角时提及
⑤ 自评闸门 5 个维度各 0–5 分,总分 <12 判"流水账" → 先重写一次 →
仍不合格就换下一条线索,并把放弃原因写进 notes/
⑥ 文笔层 骨架稿扩写成成稿:按年份场景分块扩写(目标 8000 字),
全程做 AI 味机检、标点规范化,并用专名白名单防幻觉
⑦ 本土化 英文专名换成中文译名:人名=音译+绰号意译,地名整体意译
⑧ 发布 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/v1/cast.md)。
## 三、目录
| 路径 | 内容 | 入库 |
|---|---|---|
| `output/volumes/v<N>/chapters/` | 连载正文:`NNN-….md` 是成稿,`NNN-….skeleton.md` 是过闸门的骨架稿 | ✅ |
| `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` | 写作规范(决定视角、边界、篇幅) | ✅ |
| `prompts/literary-expander.md` | 扩写规范(逐事件展开、禁用词、标点) | ✅ |
| `dfannals/glossary.py` | **冻结的译名表**(词干/姓/地名/种族) | ✅ |
| `dfannals/localize.py` | 专名本土化(渲染层,不改史料) | ✅ |
| `dfannals/deslop.py` | AI 味机械诊断(禁用词/句式/标点,按千字密度) | ✅ |
| `dfannals/normalize.py` | 标点与结构清理(引号、重复段、模型自加的小节标记) | ✅ |
| `dfannals/expand.py` | 分块大量扩写 + 专名白名单 + 段级返修 | ✅ |
| `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`(骨架与自评) |
| 扩写模型 | 同上网关的 `cn:minimax-m3`:叙事向模型写场景更贴地,推理向模型天然偏总结陈词 |
| 发布 | 现有 `~/.ssh/id_ed25519`(已注册到 Gitea 账号 gitadmin) |
**启动器**:一律用 `scripts/dfannals ...`,不要直接 `python3 -m dfannals.cli`。
会话 PATH 上的 `python3` 可能是编辑器工具链自建的 venv(没有 defusedxml,
且关闭了 user site),启动器会自动挑一个能 `import defusedxml` 的解释器。
## 五、安装
```bash
scripts/install-df.sh --download # 装 DF + DFHack(Bay12 慢,会断点续传)
sudo apt install -y python3-defusedxml
scripts/deploy-dfhack.sh /mnt/c/Users/$USER_WIN/DwarfFortress # 部署观测/驱动脚本
```
## 六、产出一卷
```bash
# 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) 逐集产出:骨架过闸门 → 分块扩写成成稿(骨架另存 .skeleton.md)
scripts/dfannals episode --index 1
scripts/dfannals episode --index 2
scripts/dfannals episode --index 3
# 7) 只跑文笔层:拿已有的某一集骨架做大量扩写(A/B 对照,不覆盖原稿)
scripts/dfannals expand --index 1 --target 8000
```
实测:**单集 3–8 分钟**(骨架写作 1 次 + 自评 1 次 + 分块扩写 5 次,若判平淡会多一次重写);
骨架稿 800–1500 字,成稿约 8000–12000 字。
## 七、配置在哪
| 想改什么 | 改哪 |
|---|---|
| 哪些互动算"关系"、权重、转折标记 | `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` |
| 扩写规格(逐事件展开、禁用词、标点) | `prompts/literary-expander.md` |
| 译名(人名/地名/种族) | `dfannals/glossary.py`——**译名必须冻结**,否则同一角色每集会换名字 |
| 扩写目标字数、分块大小、AI 味规则 | `dfannals/config.py` 的 `EXPAND_*`、`dfannals/deslop.py` 的 `HARD_WORDS`/`PATTERNS` |
| 同人同类事件的重复上限 | `dfannals/volume.py` 的 `PER_KEY_CAP` |
| 模型与网关 | `dfannals/config.py` 的 `PROVIDER` / `MODEL` / `LITERARY_MODEL` |
| 发布目标、仓库名 | `dfannals/config.py` 的 `GITEA_*` |
**API key 不入库**:运行时从 `~/.pi/agent/auth.json` 读取,只在内存中,不打印、不落盘、不进 git。
## 八、子命令
```bash
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 集(骨架 + 扩写)
--no-expand 只出骨架|--target 8000|--model 换模型
--dry-run 不落盘、不推送
scripts/dfannals expand --index N 只跑文笔层:把已有的一集大量扩写(A/B 对照)
```
测试:`for t in tests/test_*.py; do /usr/bin/python3 "$t"; done`
(六个文件:解析与事实校验 12 项、切集 7 项、闸门 6 项、AI 味机检 7 项、标点清理 9 项、专名本土化 13 项)
## 九、世界是怎么造出来的
`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` 从外部驱动更可靠 |
| 编造的地名混过校验 | 正则里的 `\b` 在 Unicode 下把汉字也当词字符,`在Mistbreath的` 这种**紧贴汉字的英文名根本不会被检查**。改成前后看「是不是英文字母」后才抓得住 |
| 校验器把 `Yeti` 报成可疑 | 种族值在史料里是全大写(`YETI`),展示形式是 `Yeti`。只改首字母不转小写就永远对不上;现在两种写法都收,且比较时忽略大小写 |
| 一集里冒出 14 处破折号 | 扩写提示词明确禁用也拦不住。确定性替换会读着别扭,改为**定点返修**:只把命中的段落发回去重写(硬伤密度 1.5→0.0/千字) |
| 正文里出现 `## 一`、孤立的「七」 | 分块扩写时模型每写一块就自制编号小节。清理阶段按「非首行的标题行/纯数字行」删除 |
| 同一句被写两遍、段落跨块重复 | 分块之间会重写上一块的结尾。清理阶段合并相邻重复段落与段内重复句子 |
| 扩写稿里满屏英文引号 | 模型用 ASCII `"`,统计对白时会误判成「没有对白」。按出现顺序配对换成中文引号 |
## 十一、闸门到底能不能判出流水账
`scripts/gate-selfcheck.py` 是这个闸门的校准证据(跑两次真实自评):
| 样本 | 总分 | 判定 |
|---|---|---|
| 故意构造的平淡稿(纯事件罗列) | 0/25 | 判平淡 ✓ |
| 场景级有戏剧本 | 16/25 | 通过 ✓ |
阈值 12 在两者之间,区分有效。注意:阈值是校准出来的,不是拍出来的——
第一版用"梗概体"当好稿样本时只得 11 分,换了场景级样本才验证出尺度没偏严。
**真实的拒绝案例**:第 3 集的骨架连换三条线索、每轮重写一次,仍然没过
(11→10、8→11、7→8 分)。管道按设计报"需要人工干预"而不是硬推。
这说明闸门在"薄材料"上会持续拒绝——正确的反应是回退到已有骨架再扩写,
而不是把阈值调低。
## 十二、当前状态与限制
- **已完成**:第 1 卷 3 集成稿(9492 / 11260 / 11869 字)+ 人物表 + 年表;
旧的两章流水账已用普通提交删除(git 历史保留)
- **每集两稿**:`NNN-….md` 是成稿,`NNN-….skeleton.md` 是过闸门的骨架稿,便于溯源
- **定时任务未启用**:cron / 开机自启等稳定运行一段时间后再落地,目前按需手动执行
- **原始数据不入库**:`data/` 已在 `.gitignore` 中
- **已知局限**:
- 关系图的边来自成对事件,独狼型人物(独自创作、被自然杀死)进不了线索挖掘
- 多参与者事件(比赛、集体迫害)会把同场者两两连边,圈子的"关系"偏共处而非互动
- 事实校验只拦得住凭空专名,**且只认英文形态**:本土化之后,编造的中文名只能靠
"发给模型的白名单 + 段级返修"来防,没有机械防线
- 闸门判的是**骨架稿**,不判扩写后的成稿。成稿有机械指标(AI 味密度、专名校验),
但戏剧性没有自动闸门
- 译名表是手工冻结的:遇到表外的新名字会原样保留英文并进待译清单,
不会自己造一个音译(宁可难看,也不让同一角色每集换名字)
## 十三、声明
《Dwarf Fortress》版权归 Bay 12 Games 所有。本仓库只包含由其生成数据的**衍生文本**,
不含游戏本体、美术资源或存档。