古籍不是上传一个 TXT 就结束了:ReadTao Content Studio 的导入与分段设计
古籍整理最容易被低估的部分,是把“文件里有文字”变成“每个公开段落都能回到具体来源”。这篇沿着 ReadTao Content Studio 的真实导入链路,拆解来源登记、原文件保存、正文与注文分层、语义分段和稳定段落身份。
在上一篇里,我先梳理了 ReadTao 的整体架构:Content Studio 负责变化中的内容,Reader 只读取已经发布的快照。
这一篇继续往前走,从一份古籍原文件开始。
本文涉及的导入接口、来源分层和分段实现,都可以在 ReadTao GitHub 仓库 中找到。
我最初也很容易把“导入古籍”想成普通后台里的文件上传:选一个 TXT,解析成字符串,按空行切段,然后保存到数据库。
真正拿 KR5 文本做过以后,我发现这条路径几乎每一步都会丢信息。
- 文件属于哪一个来源和提交版本?
- 原始字符是否被规范化代码静默修改过?
- 一行里哪些是经文,哪些是来源自带的注文?
- 一条长经文应该是一段,还是几个语义段?
- 将来发现断句不合适,怎样重做又不破坏已经发布的链接?
古籍导入不是把文本搬进数据库,而是在原始文件和公开段落之间建立一条可验证的映射。
先区分作品、底本和内容单元
ReadTao 没有把所有字段都放进一张 books 表。
这几个概念分别解决不同问题:
| 实体 | 我用它表达什么 |
|---|---|
Work | 书库里作为一部独立典籍呈现的作品 |
Edition | 作品采用的具体底本或整理版本 |
Division | 卷、章、篇,或者一篇无层级的完整正文 |
Segment | 拥有稳定身份和锚点的语义段落 |
SegmentRevision | 这个段落在某次编辑中的具体原文内容 |
《道德经》和《道德真經注》不是“同一本书有没有注释”的开关差异。后者的完整题名、文本构成和来源注文都不同,因此可以作为独立 Work 管理。同一作品内部仍然可以保存多个 Edition,避免以后增加底本时重做整个数据结构。
这里最重要的拆分是 Segment 和 SegmentRevision。
段落身份应该稳定,文字却可能继续修订。只要不是拆分或合并,修改一个异体字不应该让段落 URL 和历史引用全部失效。
来源登记比上传按钮更早
一份文件进入 Studio 以前,我先要求编辑者登记来源:
- 来源 URL;
- 仓库和文件路径;
- 提交哈希或固定修订;
- 获取时间;
- 许可或公版依据;
- 底本说明和溯源说明。
文件上传后,Backend 还会计算 SHA-256,并保存对象存储位置、大小、类型和 ETag。
浏览器不会直接拿到 MinIO 或 COS 密钥。原始文件由 Backend 代理写入私有对象存储,需要查看时再生成短时下载地址。
数据库也不保存一整份原文件大字段。它保存的是能够重新找到并验证文件的证据。
确认导入后的对象键大致按下面的结构组织:
sources/{source_id}/{sha256}/{filename}这样做的意义不是为了目录好看,而是让“这次解析使用了哪份原文件”变得可检查。只要文件内容发生一个字节变化,校验和就会不同。
我给自己的来源底线
一个公开段落至少应该能够回到作品、底本、内容单元、原始文件和发布版本。只有一段脱离来源的“干净文本”,并不等于已经完成古籍数字化。
原文件不能在解析时被覆盖
上传完成后,解析器会读取 Mandoku/Org 风格的结构标记、元数据、页码锚点和正文行。
但解析结果只是派生数据。原文件本身始终保持不变。
规范化可以处理换行、空白或确定性的显示问题,却不能借“自动纠错”的名义静默替换异体字、缺字或疑似错误。原始文本和规范文本必须能够同时查看。
这也是为什么我没有直接执行下面这种处理:
# 不应该把所有“看起来不统一”的字直接改掉再保存
normalized = auto_fix_everything(source_text)
save_as_original(normalized)更安全的思路是:
不可变原文件
├── 解析得到来源片段和坐标
├── 建议性的规范文本
└── 人工确认后的修订为什么要先做来源分层
带注古籍往往把正文和注文放在同一份文件里。如果直接把整行拿去分段,会产生两个严重问题:
- 注文可能被当成经文的一部分;
- 注文可能进入翻译、拼音或 RAG 的模型输入。
ReadTao 因此在语义分段以前增加了一道“来源分层”门禁。
Backend 当前使用四种来源角色:
class SourceFragmentRole(StrEnum):
MAIN_TEXT = "main_text"
SOURCE_COMMENTARY = "source_commentary"
PARATEXT = "paratext"
UNRESOLVED = "unresolved"| 角色 | 含义 | 后续去向 |
|---|---|---|
main_text | 典籍正文 | 进入来源单元和语义分段 |
source_commentary | 原始来源自带的注、疏、音义 | 独立锚定、修订和审校 |
paratext | 卷题、署名、跋文等非正文资料 | 保留来源,不进入正文生成 |
unresolved | 规则无法可靠判断 | 必须人工处理,不能提交 |
自动规则只给建议,还会保存规则版本、命中原因和置信度。编辑者可以在任意字符边界拆分片段,再重新指定角色。
来源注文默认锚定到前方最近的正文单元,编辑者可以调整前后位置和“注、疏、音义”等来源标签。它属于原典自身,不是 ReadTao 后加的编辑注,也不允许 AI 生成。
覆盖检查是导入链路的硬约束
只要允许在字符边界拆分,就必须回答一个问题:有没有字在处理过程中消失、重复或错位?
source_fragments 因此会保留:
- 原始行号;
- 行内起止字符位置;
- 片段顺序;
- 角色;
- 规则和人工确认信息。
同一内容单元中的非元数据字符,必须按原顺序被片段恰好覆盖一次。
接下来生成的来源文本单元和语义段落同样要检查:
- 有没有缺失单元;
- 有没有一个单元被重复使用;
- 单元顺序是否连续;
- 注文是否已经锚定;
- 是否还存在待界定内容。
这组约束看起来有些严格,但它避免了一种很难发现的错误:页面上的文本读起来仍然通顺,某个被解析器漏掉的短句却已经永远离开了公开版本。
王弼注样例怎样使用固定参考
ReadTao 当前的完整演示使用 Kanseki Repository 中的 KR5c0073,也就是王弼《道德真經注》四卷来源文件。
这类文件里的经文、王弼注文和卷内其他材料需要分开。为了减少维护者盲切,Backend 还固定了一份可追溯的维基文库修订,用来做逐字符对齐和边界建议。
这里有两个不能越过的边界:
- 参考文本只能帮助判断正文、注文和标点边界;
- 参考文本中的字不能覆盖 Kanseki 底本中的原字。
换句话说,逐字符对齐成功以后,我可以利用参考标点把一条长正文细分成更合适的来源单元,却不能因为参考文本写了另一个字,就偷偷修改登记底本。
固定参考也不会在运行时临时联网抓取。修订号、文件、许可和校验和一起保存在仓库中,保证相同输入能够重复得到相同建议。
语义分段不是按 120 个字切一刀
来源分层完成后,只有 main_text 会进入语义分段。
当前规则版本叫 classical-zh-rules-v1。它是版本化、可解释的确定性 Python 规则,不是一个小模型,也没有训练权重和推理费用。
规则会观察:
- 句号、分号等完整句边界;
- “故”“是以”“夫”等篇章转折词;
- 相邻来源单元的词汇衔接;
- 段落软长度;
- 是否出现不利于阅读的孤立短尾段。
每个候选段都会返回来源单元、边界理由和置信度,然后由 Studio 展示给编辑者。
Studio 会在候选段超过 120 个非空字符时提醒,但这只是展示和审读的软阈值:
- 人工合并产生超长候选时立即提示;
- 最终确认仍有超长段时,第一次点击只警告并暂停;
- 编辑确认语义上确实需要保留时,第二次点击可以继续。
古籍里的语义完整性不会服从一个绝对字符数。这个提醒只是帮助我发现“虽然技术上能显示,但不利于对照阅读”的长段落。
稳定段落身份怎样产生
确认导入以后,Backend 会在一个事务里创建:
Division内容单元;Segment稳定语义段落;- 第一份
SegmentRevision原文修订; - 来源文件、来源片段和段落之间的映射。
Segment 保存 source_unit_ids,这才是分段完整性检查的权威映射;source_line_numbers 只是为了溯源和兼容读取保留的派生字段。

段落锚点也不是数组下标。它采用无意义短哈希加内容单元、段落序号,例如:
c80e7dff-001-01如果只是修订文字,segment_id 和锚点保持不变。如果把一个段落拆成两个,原段落归档,新段落通过 lineage 记录来源关系。这样后续才有机会为旧链接保留重定向,而不是让所有历史引用悄悄失效。
首次导入可以重做,但有明确门禁
真实整理中,第一次分层或分段不合适很正常。完全禁止重做,会迫使编辑者重新上传文件;允许随时硬删除,又会破坏修订和发布历史。
Studio 因此提供“重新处理本卷”,但只允许在这个内容单元仍处于首次导入草稿时使用:
- 没有生成任务;
- 没有派生内容;
- 没有人工修订;
- 没有进入审校;
- 没有被任何发布快照引用。
满足条件时,系统清除首次产生的来源分层和语义段落,保留作品、底本、来源登记、原始文件和导入会话,再回到来源分层预览。
一旦已经进入后续流程,就必须通过正常修订、归档或发布回滚处理,不能再假装历史从未发生。
这条导入链真正保护的是什么
走完以后,一段公开原文不再只是数据库里的一串文字。它可以回答:
- 属于哪部作品和哪个底本;
- 来自哪份原始文件;
- 对应原文件的哪些字符区间;
- 为什么在这里形成语义边界;
- 当前文字是哪一份修订;
- 进入了哪一个发布版本。
这才是我认为 Studio 最重要的价值:不是提高“上传速度”,而是让长期、低频的个人整理仍然有证据、有状态,也能够暂停后再继续。
下一篇继续处理分段之后的内容:AI 只能写初稿:ReadTao 的内容生成、人工审校与失败重试。