{"owner":"oomol-lab","repo":"pdf-craft","hasSkills":true,"totalSkillsCount":5,"totalTokensCount":10239,"categories":["anthropic-skill","project-spec","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["references/release-workflow.md","references/architecture.md","references/conversion-pipeline.md","AGENTS.md","references/development-and-worktrees.md"],"skills":{"references/release-workflow.md":"# 发版流程\n\n**约束范围：** 发版准备、版本号对齐、变更日志要求，以及手动 GitHub Actions 发版工作流所需的仓库状态。**不约束：** 转换内部逻辑、与发版无关的依赖升级，以及手动发布 PyPI。\n\n## 发版模型\n\n发版由手动触发的 GitHub Actions 发版工作流执行。在运行该工作流之前，仓库必须先完成准备。该工作流是发版关口：它会从 `pyproject.toml` 读取版本号，检查仓库状态是否可发布，发布包到 PyPI，推送版本标签，并使用变更日志文件作为发布说明创建 GitHub Release。\n\nAgent 的工作应聚焦于准备仓库内的发版前置条件。发版准备过程中，不要手动发布 PyPI，不要手动创建 release tag，也不要手动创建 GitHub Release。\n\n## 准备发版\n\n对于目标版本 `X.Y.Z`，准备仓库时要确保以下事实成立：\n\n- [pyproject.toml](../pyproject.toml) 中的 `[project].version` 为 `\"X.Y.Z\"`。\n- `docs/changelog/vX.Y.Z.md` 存在，并包含发布说明。\n- 变更日志应包含简洁摘要；在有帮助时按类别组织变更；在可取得时链接相关 PR 或 issue；并包含完整变更日志的 compare 链接。\n- 发版准备变更应先通过普通 PR 流程审查，再从 `main` 运行手动发版工作流。\n\n发版准备 PR 应保持聚焦。不要把发版准备和无关重构或依赖变更混在一起，除非该版本本身就是围绕这些变更发布。\n\n## 验证\n\n发版准备使用常规轻量验证面：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\npoetry build\n```\n\n如果某个验证步骤无法在本地运行，应在交接说明中明确写出。\n\n## 变更日志说明\n\n发版工作流会直接读取 `docs/changelog/vX.Y.Z.md` 作为 GitHub Release 正文。变更日志文件应写成面向读者的发布说明，而不是内部实现说明。\n\n使用 `docs/changelog/` 中已有变更日志文件作为风格参考。发布标题和版本标签使用 `vX.Y.Z` 形式。\n","references/architecture.md":"# 架构与模块边界\n\n**约束范围：** 包结构、公共 API 和模块归属。**不约束：** OCR 算法细节、开发命令或发布步骤。**何时阅读：** 判断代码应放在哪里，或修改公共导入面时。\n\n## 包公共面\n\n## 新模块边界\n\n`pdf_craft` 的主要业务模块围绕可组合的文档处理阶段组织：\n\n- `extractor/`：PDF 页面、OCR、目录和章节分析的适配入口；产出 PDFCraftExtraction。\n- `document/`：PDFCraftExtraction 的 workspace/`.pcex` 存储、校验和来源位置契约。\n- `renderer/`：PDFCraftExtraction 到 Markdown 或 EPUB 的格式渲染入口。\n- `transformer/`：格式无关的 XML 内容变换，包括 LLM 翻译、结构填充和校验。\n- `pipeline/`：格式专属编排。EPUB Pipeline 将 EPUB XHTML/目录/元数据交给 Transformer；PDF Pipeline 以 replace-only 方式将 Chapter 来源区域写回 PDF。\n\n公开中间格式为 `PDFCraftExtraction`，持久化与交换载体必须是 `.pcex` ZIP。其内容为\n`manifest.json`、`pages.xml`、`chapters/`、`assets/`，以及可选 `toc.xml`、`cover.png`、\n`furnitures.xml` 和 v4 `translations/`。`translations/index.json` 按文件内唯一 ID 排列零到多个\n译文层；每层包含完整章节译文、metadata overlay、`coverage.xml`，以及可选 furniture 译文。\n目标语言只是层的元数据，同一种语言可以有多个 ID。根级 `translation.xml` 仍是旧式或内部物化\n译文视图的覆盖记录，不代表 v4 中可供渲染选择的持久化译文层。\n\nPCEX 格式版本以已经发布的 Git tag 为兼容基线，不随开发期间的中间 commit 逐次递增。只有准备\n新版本且格式相对上一个已发布版本发生 schema 变化时，才评估是否增加 `format_version`。当前 v4\n相对 `v2.3.1` 的 v3 增加了可叠加 translation layer；读取器继续接受 v1/v2/v3。\n\nPCEX chapter 的阅读流使用 `FlowItem`：`TextFlowItem` 是作者意义的正文/标题段，包含保留\n来源 bbox 的 `SourceTextFragment`，并可在 fragment 之间嵌入图片/表格 `SourceAsset`。\n`DisplayFormula` 与 `StandaloneAsset` 是独立 flow node；段落公式不是 AnchoredContent，必须\n阻断段落拼接。类名与 chapter XML 元素一一对应，详见 `docs/*/PCEX_FORMAT.md`。\nNarrative 翻译仅把嵌在 `TextFlowItem` 中的图片/表格替换为无文本的临时 anchor，不能读取或覆写\nasset 文本；`StandaloneAsset` 不伪造 paragraph anchor，并与 NarrativeFlow 隔离；\n`AnchoredContentTransformer` 是另一条按 asset 小批次工作的翻译边界，负责 title/content/caption。\n目录-backed 形态只供一键转换在 `analysing_path/extraction/` 内部衔接前后端；普通目录不是\n公开输入。`ocr/`、`plots/`、`done` 和其他 analysis 文件仅是可丢弃的诊断/恢复缓存。\n\n`pdf_craft/__init__.py` 是公共导入面。`AsyncPDFCraft` 是业务核心，`PDFCraft` 是位于\n`pdf_craft/sync/` 的同步门面；`pdf_craft/transform.py` 是 PDF 前端\n提取 engine。Markdown、EPUB、翻译和 PDF 写回后端只能读取 PDFCraftExtraction，不得读取\nanalysis/OCR 缓存。\n\n`PDFCraftExtraction` 是公开但不包含业务方法的 opaque handle。用户只能通过两个 Craft 门面的\n`open_extraction` / `export_extraction` 打开和导出，再把句柄传回门面或高级组件。高级可组合组件\n（Extractor、Renderer、Transformer、LLM runtime）仅提供无 `_async` 后缀的异步方法；同步适配\n不得进入业务核心。配置、选项、事件和数据类不拆分同步/异步版本。模型下载同样以 async 实现为准，\n同步使用通过 `PDFCraft.predownload_models()` 委派。\n\n业务执行以 async 为唯一真源：请求、重试、repair loop、编排和转换不能再提供第二套同步实现。\n`run_sync` 只能由 `pdf_craft/sync/` 的用户门面调用；禁止在实现层启动事件循环把同步业务伪装成\n异步业务。用户提供的 LLM、Transformer、TOC 转换、PDF handler 和图片 URL resolver 执行方法\n必须是异步契约，不提供同步扩展适配。普通事件通知可以接受同步或异步 callback。\n仅当第三方库没有异步接口时，才在独立执行域中调用同步适配器；适配器只承担第三方调用、\n文件读写和必要资源清理，不复制业务编排。跨域 callback 桥接不得成为另一条请求或转换路径。\n\n`translate_extraction()` 保留 source layer 并追加 replacement-only 译文层。Markdown/EPUB 渲染\n通过 `RenderMode.SOURCE`、`REPLACE`、`BILINGUAL` 决定只读原文、选择一份译文或合并原译文；\n`BILINGUAL_TEXT` 在同一来源 fragment 内追加译文。这些译文模式可显式传 translation ID，省略时稳定选择 index 第一项。PDF 写回也可显式选择译文层，\n但仅消费已有正文和 furniture 写回能力，不写回 anchored 图片/表格文字。\n\n除非任务明确要求破坏性 API 变更，否则把以下名称和默认值视为公共 API：\n\n- `PDFCraft`、`AsyncPDFCraft`、`PDFOptions`、`ExtractionOptions`、`PDFCraftExtraction`\n- `PDFExtractor`、`MarkdownRenderer`、`EpubRenderer`\n- `ExtractionTransformer`、`NarrativeXMLTransformer`\n- `AnchoredContentExtractionTransformer`、`AnchoredContentTransformer`、`AnchoredContentXMLTransformer`\n- `predownload_models`\n- `LLM`\n- `JEV`、`FootnoteOptions`、`FootnoteRefinement`\n- `DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`\n- `DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`\n- `AsyncPDFHandler`、`AsyncPDFDocument`\n- `BookMeta`、`TableRender`、`LaTeXRender`\n\n## 模块归属\n\n- `pdf_craft/pdf/` 负责 PDF 元数据、渲染、页引用、通过 `doc-page-extractor` 接入 OCR 后端，以及 OCR 页 XML 数据。\n- `pdf_craft/extractor/toc/` 负责目录页检测和标题层级分析，包括可选的 LLM 辅助分析。\n- `pdf_craft/extractor/chapter/` 负责根据 OCR 页 XML 和 TOC 事实生成章节结构。\n- `pdf_craft/markdown/` 负责 Markdown 段落解析和 Markdown 输出渲染。\n- `pdf_craft/renderer/epub/` 负责把章节数据转换为 `epub-generator` 的记录并生成 EPUB。\n- `pdf_craft/llm/` 负责增强目录分析所需的可选 LLM 调用。核心转换应保持不依赖该增强能力也可使用。\n- `pdf_craft/common/` 负责可复用的文件系统、XML、资源和统计辅助逻辑。\n\n## 外部包边界\n\n`doc-page-extractor` 和 `epub-generator` 是被 pin 住的运行时依赖。它们内部的问题通常应在各自仓库修复，再通过版本升级或明确的本地联调引入。本仓库 `scripts/` 下的同步脚本会覆盖 `.venv` 中已安装的依赖源码；这些脚本只是手动本地联调辅助，不是普通项目 setup。\n\npdf-craft 对外只暴露自己的 OCR 配置对象，不暴露 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 注入口。需要新增 OCR 后端时，优先在 `doc-page-extractor` 增加官方构造入口，再在 pdf-craft 映射成封闭配置对象。\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不要把 `torch` 或 `torchvision` 作为 pdf-craft 的直接运行时依赖；用户仍可能需要按自己的环境覆盖安装 CPU 或 CUDA wheel。\n","references/conversion-pipeline.md":"# 转换流水线\n\n**约束范围：** PDF 到输出文件的流程、中间产物和转换契约。**不约束：** 通用 setup 或打包。**何时阅读：** 修改提取、TOC、章节生成、Markdown 渲染或 EPUB 渲染时。\n\n## 运行时流程\n\n## 组合流程\n\nExtractor 生成 PDFCraftExtraction 后，Renderer 可直接生成 Markdown 或 EPUB。可选 Transformer\n可以在渲染前修改结构化文本；`pipeline/epub` 也可把既有 EPUB 的 XHTML、目录和元数据交给\n同一个 XML Transformer。PDF Translation Pipeline 只处理已记录来源 bbox 内的文本。\n\nPCEX 翻译不再覆盖 source：`translate_extraction()` 在 v4 `translations/` 下追加一个完整的\nreplacement-only 层。Markdown/EPUB 随后选择 source、某一译文替换或 source 加某一译文；选择\n依赖 translation ID，未指定时使用 index 第一项。这个渲染选择与直接 EPUB 输入的 append/replace\n提交模式是不同阶段的概念。一键 PDF→Markdown/EPUB 也复用 replacement-only 译文层，显式\n选择本次新生成的 ID；REPLACE / APPEND_TEXT / APPEND_BLOCK 分别映射为 REPLACE /\nBILINGUAL_TEXT / BILINGUAL 渲染。只有临时展示视图组合原译文，不把重复来源写入待校验\nPCEX。BILINGUAL_TEXT 在同一来源 fragment 内追加内容；BILINGUAL 按正文段追加译文。\n资产先按稳定 flow/child 索引组合，再插入双语段落；表格在单元格内组合，不能追加第二张表。PDF Translation Pipeline 也可通过显式 translation ID 选择持久化译文；\n省略 ID 时继续消费根内容。PDF 写回只处理 narrative 与 furniture，不处理缺少字段级来源坐标的\nanchored 图片/表格文字。\n\n`AsyncPDFCraft.convert_pdf_to_markdown()` 和 `AsyncPDFCraft.convert_pdf_to_epub()` 会先提取到内部 workspace，\n再渲染目标输出。提取流程是：\n\n1. 通过 `PDFHandler` 渲染 PDF 页面。\n2. 通过 `OCR.recognize()` 识别页面布局。\n3. 在 `analysing_path/ocr/` 写入 OCR 页 XML 和恢复缓存，在\n   `analysing_path/extraction/assets/` 写入资源。\n4. OCR 的终态页事件经磁盘流恢复输入页序，传统跨页解析与 OCR 并行推进；闭合正文、脚注和\n   未定归属来源存入磁盘数据库。可选 `on_body` / `on_footnote` 交付独立 PCEX XML 快照，\n   回调背压由磁盘队列与 OCR 生产隔离。EOF 排空后再分析 TOC 数据。\n5. 在 `analysing_path/extraction/` 生成章节、TOC、页面几何和 manifest；章节使用 PCEX v3\n   FlowItem，保留文字 fragment、段内 anchored image/table 与独立段落公式的阅读顺序。\n6. 仅从该 extraction 渲染 Markdown 或 EPUB。\n\n章节生成在第 5 步内部先为 TOC 排除后的 OCR Page 建立完整来源记录：保留原文、原分类、\nOCR 流内下标和独立阅读位置。正文沿用原 source_order；新提取的脚注使用正文布局数作为\n偏移，写入同一页的 fragment 身份空间。OCR Page 的 order 不变，旧 PCEX 不重新编号。\n阅读顺序保留正文已有的栏序，将脚注分类中的来源按同栏位置插入，不按全页 y 坐标排序。\n通宽锚点限定阅读带；栏尾来源插在所属栏最后成员之后，不能退回整页或整带末尾。\n传统 join、citation 拆分和同页 ref 匹配只建立初始结论，再投影为 PageAnalysis。\n跨页合并必须把页尾/续页页首 image/table 保留在两侧文字之间，不可挪到整段之后。\nPageAnalysis 的 continuation 表示文字前驱，跳过同一归属流中的图片/表格；资产自身 gap\n保持 false，DisplayFormula 才是硬边界。投影与恢复必须保留段内锚点，精修与校验遵守同一规则。印刷标记的\n完整来源和前缀范围保留到组装：正文投影保留前缀，定义投影才移除前缀。断词修复延后到 ownership\n组装完成后的语义投影；原始文字记录不被改写，投影为空的 fragment 也保留原身份和 bbox。\n\n默认传统算法由 `extractor/chapter/stream.py` 增量连接、来源投影和磁盘组装；无目录页排除时复用\n在线生成的内容数据库，有目录页排除时从 OCR 缓存按排除规则重放。章节定稿逐章处理。\n需要可编辑页级 ownership 的 PageAnalysisProcessor 仍使用 `restore_streams` 的整批事务语义；\n在线回调发生在可选 refinement 之前，不声明是修正后归档的重放。\nFootnoteOptions(refinement=FootnoteRefinement(...)) 在调用方 event loop 在线执行 JEV 筛选及\nLLM 页级修正；CLI 的 --footnotes 只启用传统算法。LLM JSON 通过 request_guaranteed_json\n编译 ownership、必需元数据、引用位置及定义关系，候选在完整暂存对象图上组装成功才接纳。\n候选在暂存对象图通过校验后才提交；允许降级的页保留算法结果，必要时撤销整批精修，任何失败均不修改调用方原对象。不得额外嵌套请求重试。\nref 仍以 layout_id + citation_index + before/mark/after 定位，不要求 LLM 算 offset。\n\n连接的唯一编辑真源为后继的 continues_from_previous；continues_to_next 由此派生，旧字段若\n显式返回，仅作为同页一致性断言。没有按修正先后顺序选择布尔值的 reconciliation。\n已知跨页定义 ID、起点索引与既有跨页片段不可由单页请求删除或重命名。保护同时覆盖 segment\n记录与既有 layout 成员，不能保留 ID 却将成员改为 ignored、正文或其他定义；内部自然段边界仍可编辑。页内非共享定义可以\n通过重分组拆分或合并，新增 ID 为 new-页码-index；未确定归属的续段可明确连接到上一页已知\n定义。无索引段排在本页有索引段前，有索引定义从 1 连续递增。没有起点的已知 continuation\n是非法对象图；unresolved 来源则显式保留为可读正文，不能猜测归属后丢弃。\n\n一条定义可以有零个或多个引用出现；数字、圈号及星号都属于印刷标记域，标记不是定义身份。\n未匹配定义由 Chapter.definitions 保留，经既有 PCEX references 容器编码，无需制造正文 ref。\nMarkdown/EPUB 保留这些定义。正文及脚注 TextFlowItem 共用 narrative_texts 遍历，翻译 coverage\n和 PDF 计划消费同一来源映射；有 coverage 的脚注正文实际回填，无 coverage 的区域仍作障碍。\n多次引用或跨章节复用同一来源不重复回填。定义开头的印刷标记在 PDF 输出时恢复。\n格式专有适配留在 PCEX 层，通用 XML translator 无需了解脚注。\n\nPCEX 格式保持兼容。现有归档和译文层按原身份读取、校验和导出，不做推测重编号；\n缺失 coverage 继续表示 preserved。旧归档若同一章节已有歧义的重复 fragment 身份，仍明确拒绝，\n不声称能够无损迁移。更详细的修复前后证据见 docs/verification/footnote-788.md。\n\n当未传入 `analysing_path` 时，`EnsureFolder` 会创建临时目录。当传入该路径时，它会成为可持久复用的缓存和调试输出目录。\n\n## 中间产物契约\n\nanalysis 与稳定 extraction 明确分离：\n\n- `ocr/page_*.xml`：OCR 页数据。\n- `ocr/done`：表示所有选中页面已完成识别的标记。\n- `plots/`：启用 plot 生成时的可选可视化调试输出。\n- `extraction/manifest.json`：格式版本、producer、创建时间和文档元数据。\n- `extraction/pages.xml`：1-based OCR 像素坐标空间、实际渲染 DPI 和逐页像素宽高。\n- `extraction/assets/`：按内容 hash 存放裁剪出的图片、公式和表格。\n- `extraction/chapters/chapter_*.xml`：生成的 v3 FlowItem 章节记录及原 PDF page/bbox 映射；\n  图表可嵌在 TextFlowItem fragment 之间，DisplayFormula 必须独立且不能被段落拼接跨越。\n- `extraction/toc.xml`、`extraction/cover.png`：可选目录和封面。\n- `extraction/furnitures.xml`：可选的页面家具 pattern 与页级 section。\n- `extraction/translations/index.json`：v4 可选译文索引；条目以 ID 区分，记录目标语言和创建时间。\n- `extraction/translations/<id>/chapters/`、`metadata.json`、`coverage.xml`：一份独立译文层；章节\n  结构与 source 对齐，coverage 记录 Narrative、furniture position/section 及独立图片/表格文本的\n  `translated` / `preserved` 状态。可选 `furnitures.xml` 保存该层的 furniture 译文。\n- `extraction/translation.xml`：旧式或内部临时物化译文视图的根级 coverage；不要把它当作 v4\n  translation layer 索引。\n\nNarrative 翻译中的段内 asset 只以无文本临时 anchor 保持位置；独立 asset 不伪造 anchor。图片/\n表格 title/content/caption 属于独立翻译阶段；标准高层翻译入口固定先运行 NarrativeFlow，随后由\n`with_anchored` 决定是否运行该阶段，并以 anchored coverage 约束 v4 translation layer 中允许变化的 slot。\n\n公共分段流程把 `extraction/` 打包为 `.pcex`；恢复后端只接受 `.pcex` 或已加载的\n`PDFCraftExtraction`。一键转换直接使用 workspace，只有显式 `extraction_path` 时才额外导出\n`.pcex`，避免压缩往返。翻译后的 `.pcex` 必须保留 source 的 manifest、pages、章节、TOC、封面、\nfurniture 和资源，并把每份译文及其 coverage 隔离在对应 translation layer 中。\n`PDFCraftExtraction` 只作为 opaque handle 流转；归档的打开、校验和导出由 `AsyncPDFCraft`\n负责，`PDFCraft` 仅同步转发这些操作。\n\n修改 XML schema、文件命名或跳过语义会影响多个模块，应视为跨流水线变更，并配套有针对性的测试。\n\nMarkdown/EPUB 只从 FlowItem 的阅读顺序渲染，不能也不应复刻固定 PDF 页面几何：Markdown 将\n段内图片/表格安全拆成块级内容；EPUB 默认同样处理，仅在小型图片的相邻 fragment 提供同页、\n另一侧、纵向重叠等强几何证据时尝试可降级的左右 float。表格和 DisplayFormula 不得 float；\n阅读器不支持 CSS 或窄屏时仍必须保留正常块级顺序。\n\n## 重型运行时边界\n\n`PageExtractorNode` 会延迟导入 `doc-page-extractor`，并且只在需要 OCR 时根据 pdf-craft 的 OCR 配置创建上游 extractor。除非任务明确要求 eager loading，否则应保持这种延迟加载行为。\n\nOCR 配置是封闭公共面：`DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`、`DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`。不要把 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 作为 pdf-craft 公共注入口。\n\n本地 OCR 可能需要 Poppler、支持 CUDA 的 PyTorch、大型模型下载和较高显存；供应商 OCR 不需要本地 CUDA，但需要网络和密钥。普通单元测试应保持不依赖这些资源也能运行。\n\n## 错误与恢复语义\n\n失败决策、可信译文缺失与任务级成功的原则见 [失败与有效成果](failure-and-completion.md)。页面级降级会保留图片或明确占位，全部主要工作降级仍须报错。\n\n已存在的 `page_*.xml` 会被跳过。`done` 标记会让 OCR 识别整体跳过。修改恢复行为时要谨慎，因为它同时影响本地手动运行和 VGE worktree 重跑。\n\n## EPUB 原子翻译回调\n\nEPUB 适配器异步逐项读取任务，在共享 XMLTranslator 的局部映射采纳位置接收结果，按章写入\n临时归档；不收集全书正文任务或完整译文结果。同步 iterable 的既有核心接口继续供 PCEX 翻译使用。\n可选 `on_body` 接收 `EpubFragmentContent(epubcfi, original, translated)`，只包含去掉窗口上下文后的\n原子片段；来源 CFI 索引由 EPUB 适配器按需构建，源范围贯穿公式替换。`pipeline/epub/cfi.py`\n只负责文档内定位，OPF/spine 前缀由外层提供。用户说明见 `docs/*/STREAMING.md`。\n\n\n## PCEX 内容流与页面查询\n\n`document/content.py` 逐 XML 内容项解码现有 FlowItem/Reference，body 中引用只携带身份，完整\n定义走独立条目。`document/stream_view.py` 提供固定资源/目录/页面几何与原文/所选层/在线译文流；\n标准 XMLTranslator 不先生成全书译文 workspace。`transformer/content.py` 复用同一引擎，保留\n资源前后文，PCEX writer 是该流的一个消费者。自定义整章 translator 保留显式兼容路径。\n\nMarkdown 逐项写暂存内容，EPUB 逐项转换并 spooling EPUB records，PDF 以来源顺序的磁盘\nfeed 向隔离 Qt 进程提交替换项。PDF 真实布局通过磁盘回传，跨页段落在所有区域排版定稿后\n一次通知；窗口只按现有页级约束收尾，不拿内容回调冒充排版通知。\n\n公共 `document/paragraph.py` 定义 SourceRegion、ParagraphContent、PDFSourceRegion 与\nPDFParagraphContent。翻译/重排渲染使用独立 ParagraphContent（Markdown text、可选 original、\n来源 regions），PDF 使用每区域带字号/行/公式/擦除的 PDFParagraphContent。提取仍保留原有\nXML 回调；EPUB 输入翻译仍保留 EpubFragmentContent。未提供通道时不封装该通道的公共产物。\nSourceRegion.page 复用 PageGeometry，source_order_in_page 对应 PCEX source_order；bbox\n仍为 OCR 像素整数，map_bbox 根据实际页面尺寸返回 float。render_dpi 是请求值，不能单独\n作为缩放依据。PDF 公共布局的坐标与字号也换算为来源 OCR 像素，不引入 font_id。\n\n`document/access.py` 的 read_pages/read_asset 与 ExtractionContext 继续作为公共几何/资源\n访问入口。内容对象包含页面几何，因此回调消费者不必再做页面联表。资源只通过受支持的\n字节读取入口获取，不依赖临时目录。\n\nLLM 有序交付使用磁盘重排缓存；异步任务上下文、来源 DOM 与完成结果都可落盘，通过\n稳定 root/node identity 在当前文档交付时恢复映射，队首慢请求不再保留全书原文 DOM。\n同步 iterable/自定义整章契约保留原对象身份。双阶段 vendor OCR 续阶段优先，未确定归属来源\n按阅读顺序发出，延续 #794/#795 回归。\n\n\n元信息通知复用 `document/metadata.py` 的 MetadataContent/DocumentMetadata。\nPDF 原生-only 通知在 OCR 前，LLM 提取通知在字段合并后；异常不能被提取降级逻辑吞掉。\nPCEX 的共享 NarrativeXMLTransformer 元信息阶段负责翻译通知，EPUB 适配器负责 OPF 视图通知；\n书籍通知不包含 TOC 或文档 head/title。只有用户提供 on_metadata 时才构造独立公共快照。\nEpubTranslation 仅作为 EpubFragmentContent 的兼容别名。\n","AGENTS.md":"# Agent 工作流\n\npdf-craft 是一个把扫描书籍 PDF 转换为 Markdown 或 EPUB 的 Python 库。本仓库使用 `~/.agents/skills/vibecoding` 作为通用维护工作法。本文件只记录 pdf-craft 特有的边界和按需阅读路由。\n\n## 工作区边界\n\n- `pdf_craft/` 是包源码。公共导入从 `pdf_craft/__init__.py` 暴露，便利入口在 `pdf_craft/functions.py`。\n- `tests/` 包含轻量单元测试和小型 PDF fixture。普通代码改动默认以这些测试作为验证面。\n- `docs/` 和 `README.md` 是读者/贡献者阅读的文档。不要把这些说明复制到 Agent 文档里。\n- `references/` 是 Agent 面向的引用文档。只阅读当前任务需要的引用文档。\n- `pdf_craft_tool/` 是未发布的本地 CLI，承载手动转换、翻译和冒烟矩阵；`scripts/` 只保留依赖源码同步辅助脚本。不要把它们当作默认开发流程。\n- `analysing/`、`pdf-craft-output/`、`models-cache/`、`.venv/`、`dist/`、`build/` 和 `*.egg-info` 是生成产物或本地运行产物。\n\n## 仅在需要时阅读\n\n- 当需要判断模块归属、公共 API 边界或新代码应放在哪里时，阅读[架构与模块边界](references/architecture.md)。\n- 当修改 PDF 提取、OCR 归一化、缓存 XML 产物、目录生成、章节生成、Markdown 渲染或 EPUB 渲染时，阅读[转换流水线](references/conversion-pipeline.md)。\n- 当修改 PDF 回填、QTextLayout、字号拟合、行数/单行规则或 PDF 文本层排版时，阅读[两阶段 PDF 文本排版](references/pdf-text-layout.md)。\n- 当选择 setup、验证、worktree 行为、发布或外部依赖处理方式时，阅读[开发与 Worktree](references/development-and-worktrees.md)。\n- 当准备发版、更新版本号、编写 changelog 或调整发布流程时，阅读[发版流程](references/release-workflow.md)。\n\n- 当修改失败恢复、翻译缺失、错误定位或任务成功判断时，阅读[失败与有效成果](references/failure-and-completion.md)。\n\n## 项目特有默认规则\n\n- 这是库项目。除非未来任务引入长期服务，否则不要启动常驻开发服务器。\n- 普通验证应避免 CUDA、模型下载、网络请求和完整 PDF 转换，除非任务明确触及这些行为。\n- 模型缓存和转换输出不得进入提交内容。`pdf_craft_tool` 的产物默认写入每个 worktree 自己的 `pdf-craft-output/`；只有任务明确需要 OCR 时，才考虑共享外部模型缓存。\n- 纯文档任务不得修改包代码或依赖版本。\n","references/development-and-worktrees.md":"# 开发与 Worktree\n\n**约束范围：** setup、验证、VGE worktree 行为和发布命令。**不约束：** 包架构或转换内部逻辑。**何时阅读：** 选择命令、编辑项目配置或修改开发文档时。\n\n## 环境\n\n使用 Python `>=3.11,<3.14`；默认开发版本是 Python 3.11。本仓库使用 Poetry 2.x；配置 `poetry config virtualenvs.in-project true` 时，虚拟环境应位于当前 worktree 内。\n\n普通开发安装依赖：\n\n```bash\npoetry config virtualenvs.in-project true\npoetry install --with dev\n```\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不直接声明 `torch` 或 `torchvision`。只有当任务需要指定 CPU 或 CUDA wheel 时，才按当前环境覆盖安装 `torch` 和 `torchvision`。\n\n真实 OCR 有六种配置入口：\n\n- `DeepSeekOCRLocalConfig`：本地 DeepSeek OCR，真实转换需要 CUDA。\n- `DeepSeekOCR2LocalConfig`：本地 DeepSeek OCR 2，真实转换需要 CUDA。\n- `UnlimitedOCRLocalConfig`：本地 Unlimited OCR，真实转换需要 CUDA。\n- `DeepSeekOCRVendorConfig`：DeepSeek OCR 供应商模式。\n- `DeepSeekOCR2VendorConfig`：DeepSeek OCR 2 供应商模式。\n- `UnlimitedOCRVendorConfig`：Unlimited OCR 供应商模式。\n\n库代码不得自动读取 `.env`。未发布的 `pdf_craft_tool` CLI 通过 `pdf_craft_tool/runtime.py` 加载工作区 `.env` 并创建显式配置对象。所有手动运行时变量使用 `PDF_CRAFT_` 前缀：OCR 使用 `PDF_CRAFT_OCR_MODE` 和对应的 `PDF_CRAFT_*_OCR_*` 配置；文本 LLM 通过可复用的 `PDF_CRAFT_LLM_<PROFILE>_*` profile 配置。默认 profile 可在运行时从 `oo llm config --json` 获取 OOMOL 凭据，不能复用 OCR-only endpoint。\n\n## 默认验证\n\n优先选择能跨越改动边界的最小命令集：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\n```\n\n`poetry build` 用于验证打包。`scripts/` 中的完整转换脚本是手动检查，可能需要 Poppler、PyTorch、模型缓存和 CUDA。\n\n## VGE Worktree 行为\n\n本项目没有长期运行的开发服务，因此 `.conductor/settings.toml` 不应定义 `scripts.run`，除非未来项目新增 watcher 或 server。VGE setup 同时负责安装依赖并初始化当前 worktree 的 `.env`，确保当前 worktree 中的 pdf-craft 及其 `doc-page-extractor` 依赖可以被导入和测试。\n\n`.env` 是 worktree 私有运行配置，不提交到 Git。VGE setup 在当前 worktree 缺少 `.env` 时，应优先从源工作区的 `.env` 复制，以便继承供应商 OCR 密钥和本机开发配置；只有源工作区也没有 `.env` 时，才从 `.env.template` 创建空配置。\n\n默认不配置 `scripts.archive`。VGE 会负责释放 worktree；项目级 Cleanup 只适合需要保留现场后执行额外归档或收尾动作的项目，本仓库当前没有这种需求。\n\n`pdf_craft_tool` 生成的转换输出默认留在当前 worktree 的 Git 忽略目录 `pdf-craft-output/` 下。模型缓存可能很大；只有在有意进行本地 OCR 工作时才使用 `models-cache/`，不要提交它，也不要假设其中已有内容。\n\n## 人类阅读文档\n\n面向人类贡献者的开发说明位于 `docs/DEVELOPMENT.md` 和 `docs/DEVELOPMENT_zh-CN.md`。README 文件应聚焦库使用者。Agent 面向的路由和项目约束保留在 `AGENTS.md` 和 `references/` 中。\n\n## 发布\n\n发版准备流程位于 [release-workflow.md](release-workflow.md)，各版本发布说明位于 `docs/changelog/`。除非任务明确要求发布或升级依赖，否则普通 worktree 支持改造不应修改包版本、依赖 pin 或发布元数据。\n"},"files":{"references/release-workflow.md":"# 发版流程\n\n**约束范围：** 发版准备、版本号对齐、变更日志要求，以及手动 GitHub Actions 发版工作流所需的仓库状态。**不约束：** 转换内部逻辑、与发版无关的依赖升级，以及手动发布 PyPI。\n\n## 发版模型\n\n发版由手动触发的 GitHub Actions 发版工作流执行。在运行该工作流之前，仓库必须先完成准备。该工作流是发版关口：它会从 `pyproject.toml` 读取版本号，检查仓库状态是否可发布，发布包到 PyPI，推送版本标签，并使用变更日志文件作为发布说明创建 GitHub Release。\n\nAgent 的工作应聚焦于准备仓库内的发版前置条件。发版准备过程中，不要手动发布 PyPI，不要手动创建 release tag，也不要手动创建 GitHub Release。\n\n## 准备发版\n\n对于目标版本 `X.Y.Z`，准备仓库时要确保以下事实成立：\n\n- [pyproject.toml](../pyproject.toml) 中的 `[project].version` 为 `\"X.Y.Z\"`。\n- `docs/changelog/vX.Y.Z.md` 存在，并包含发布说明。\n- 变更日志应包含简洁摘要；在有帮助时按类别组织变更；在可取得时链接相关 PR 或 issue；并包含完整变更日志的 compare 链接。\n- 发版准备变更应先通过普通 PR 流程审查，再从 `main` 运行手动发版工作流。\n\n发版准备 PR 应保持聚焦。不要把发版准备和无关重构或依赖变更混在一起，除非该版本本身就是围绕这些变更发布。\n\n## 验证\n\n发版准备使用常规轻量验证面：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\npoetry build\n```\n\n如果某个验证步骤无法在本地运行，应在交接说明中明确写出。\n\n## 变更日志说明\n\n发版工作流会直接读取 `docs/changelog/vX.Y.Z.md` 作为 GitHub Release 正文。变更日志文件应写成面向读者的发布说明，而不是内部实现说明。\n\n使用 `docs/changelog/` 中已有变更日志文件作为风格参考。发布标题和版本标签使用 `vX.Y.Z` 形式。\n","references/architecture.md":"# 架构与模块边界\n\n**约束范围：** 包结构、公共 API 和模块归属。**不约束：** OCR 算法细节、开发命令或发布步骤。**何时阅读：** 判断代码应放在哪里，或修改公共导入面时。\n\n## 包公共面\n\n## 新模块边界\n\n`pdf_craft` 的主要业务模块围绕可组合的文档处理阶段组织：\n\n- `extractor/`：PDF 页面、OCR、目录和章节分析的适配入口；产出 PDFCraftExtraction。\n- `document/`：PDFCraftExtraction 的 workspace/`.pcex` 存储、校验和来源位置契约。\n- `renderer/`：PDFCraftExtraction 到 Markdown 或 EPUB 的格式渲染入口。\n- `transformer/`：格式无关的 XML 内容变换，包括 LLM 翻译、结构填充和校验。\n- `pipeline/`：格式专属编排。EPUB Pipeline 将 EPUB XHTML/目录/元数据交给 Transformer；PDF Pipeline 以 replace-only 方式将 Chapter 来源区域写回 PDF。\n\n公开中间格式为 `PDFCraftExtraction`，持久化与交换载体必须是 `.pcex` ZIP。其内容为\n`manifest.json`、`pages.xml`、`chapters/`、`assets/`，以及可选 `toc.xml`、`cover.png`、\n`furnitures.xml` 和 v4 `translations/`。`translations/index.json` 按文件内唯一 ID 排列零到多个\n译文层；每层包含完整章节译文、metadata overlay、`coverage.xml`，以及可选 furniture 译文。\n目标语言只是层的元数据，同一种语言可以有多个 ID。根级 `translation.xml` 仍是旧式或内部物化\n译文视图的覆盖记录，不代表 v4 中可供渲染选择的持久化译文层。\n\nPCEX 格式版本以已经发布的 Git tag 为兼容基线，不随开发期间的中间 commit 逐次递增。只有准备\n新版本且格式相对上一个已发布版本发生 schema 变化时，才评估是否增加 `format_version`。当前 v4\n相对 `v2.3.1` 的 v3 增加了可叠加 translation layer；读取器继续接受 v1/v2/v3。\n\nPCEX chapter 的阅读流使用 `FlowItem`：`TextFlowItem` 是作者意义的正文/标题段，包含保留\n来源 bbox 的 `SourceTextFragment`，并可在 fragment 之间嵌入图片/表格 `SourceAsset`。\n`DisplayFormula` 与 `StandaloneAsset` 是独立 flow node；段落公式不是 AnchoredContent，必须\n阻断段落拼接。类名与 chapter XML 元素一一对应，详见 `docs/*/PCEX_FORMAT.md`。\nNarrative 翻译仅把嵌在 `TextFlowItem` 中的图片/表格替换为无文本的临时 anchor，不能读取或覆写\nasset 文本；`StandaloneAsset` 不伪造 paragraph anchor，并与 NarrativeFlow 隔离；\n`AnchoredContentTransformer` 是另一条按 asset 小批次工作的翻译边界，负责 title/content/caption。\n目录-backed 形态只供一键转换在 `analysing_path/extraction/` 内部衔接前后端；普通目录不是\n公开输入。`ocr/`、`plots/`、`done` 和其他 analysis 文件仅是可丢弃的诊断/恢复缓存。\n\n`pdf_craft/__init__.py` 是公共导入面。`AsyncPDFCraft` 是业务核心，`PDFCraft` 是位于\n`pdf_craft/sync/` 的同步门面；`pdf_craft/transform.py` 是 PDF 前端\n提取 engine。Markdown、EPUB、翻译和 PDF 写回后端只能读取 PDFCraftExtraction，不得读取\nanalysis/OCR 缓存。\n\n`PDFCraftExtraction` 是公开但不包含业务方法的 opaque handle。用户只能通过两个 Craft 门面的\n`open_extraction` / `export_extraction` 打开和导出，再把句柄传回门面或高级组件。高级可组合组件\n（Extractor、Renderer、Transformer、LLM runtime）仅提供无 `_async` 后缀的异步方法；同步适配\n不得进入业务核心。配置、选项、事件和数据类不拆分同步/异步版本。模型下载同样以 async 实现为准，\n同步使用通过 `PDFCraft.predownload_models()` 委派。\n\n业务执行以 async 为唯一真源：请求、重试、repair loop、编排和转换不能再提供第二套同步实现。\n`run_sync` 只能由 `pdf_craft/sync/` 的用户门面调用；禁止在实现层启动事件循环把同步业务伪装成\n异步业务。用户提供的 LLM、Transformer、TOC 转换、PDF handler 和图片 URL resolver 执行方法\n必须是异步契约，不提供同步扩展适配。普通事件通知可以接受同步或异步 callback。\n仅当第三方库没有异步接口时，才在独立执行域中调用同步适配器；适配器只承担第三方调用、\n文件读写和必要资源清理，不复制业务编排。跨域 callback 桥接不得成为另一条请求或转换路径。\n\n`translate_extraction()` 保留 source layer 并追加 replacement-only 译文层。Markdown/EPUB 渲染\n通过 `RenderMode.SOURCE`、`REPLACE`、`BILINGUAL` 决定只读原文、选择一份译文或合并原译文；\n`BILINGUAL_TEXT` 在同一来源 fragment 内追加译文。这些译文模式可显式传 translation ID，省略时稳定选择 index 第一项。PDF 写回也可显式选择译文层，\n但仅消费已有正文和 furniture 写回能力，不写回 anchored 图片/表格文字。\n\n除非任务明确要求破坏性 API 变更，否则把以下名称和默认值视为公共 API：\n\n- `PDFCraft`、`AsyncPDFCraft`、`PDFOptions`、`ExtractionOptions`、`PDFCraftExtraction`\n- `PDFExtractor`、`MarkdownRenderer`、`EpubRenderer`\n- `ExtractionTransformer`、`NarrativeXMLTransformer`\n- `AnchoredContentExtractionTransformer`、`AnchoredContentTransformer`、`AnchoredContentXMLTransformer`\n- `predownload_models`\n- `LLM`\n- `JEV`、`FootnoteOptions`、`FootnoteRefinement`\n- `DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`\n- `DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`\n- `AsyncPDFHandler`、`AsyncPDFDocument`\n- `BookMeta`、`TableRender`、`LaTeXRender`\n\n## 模块归属\n\n- `pdf_craft/pdf/` 负责 PDF 元数据、渲染、页引用、通过 `doc-page-extractor` 接入 OCR 后端，以及 OCR 页 XML 数据。\n- `pdf_craft/extractor/toc/` 负责目录页检测和标题层级分析，包括可选的 LLM 辅助分析。\n- `pdf_craft/extractor/chapter/` 负责根据 OCR 页 XML 和 TOC 事实生成章节结构。\n- `pdf_craft/markdown/` 负责 Markdown 段落解析和 Markdown 输出渲染。\n- `pdf_craft/renderer/epub/` 负责把章节数据转换为 `epub-generator` 的记录并生成 EPUB。\n- `pdf_craft/llm/` 负责增强目录分析所需的可选 LLM 调用。核心转换应保持不依赖该增强能力也可使用。\n- `pdf_craft/common/` 负责可复用的文件系统、XML、资源和统计辅助逻辑。\n\n## 外部包边界\n\n`doc-page-extractor` 和 `epub-generator` 是被 pin 住的运行时依赖。它们内部的问题通常应在各自仓库修复，再通过版本升级或明确的本地联调引入。本仓库 `scripts/` 下的同步脚本会覆盖 `.venv` 中已安装的依赖源码；这些脚本只是手动本地联调辅助，不是普通项目 setup。\n\npdf-craft 对外只暴露自己的 OCR 配置对象，不暴露 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 注入口。需要新增 OCR 后端时，优先在 `doc-page-extractor` 增加官方构造入口，再在 pdf-craft 映射成封闭配置对象。\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不要把 `torch` 或 `torchvision` 作为 pdf-craft 的直接运行时依赖；用户仍可能需要按自己的环境覆盖安装 CPU 或 CUDA wheel。\n","references/conversion-pipeline.md":"# 转换流水线\n\n**约束范围：** PDF 到输出文件的流程、中间产物和转换契约。**不约束：** 通用 setup 或打包。**何时阅读：** 修改提取、TOC、章节生成、Markdown 渲染或 EPUB 渲染时。\n\n## 运行时流程\n\n## 组合流程\n\nExtractor 生成 PDFCraftExtraction 后，Renderer 可直接生成 Markdown 或 EPUB。可选 Transformer\n可以在渲染前修改结构化文本；`pipeline/epub` 也可把既有 EPUB 的 XHTML、目录和元数据交给\n同一个 XML Transformer。PDF Translation Pipeline 只处理已记录来源 bbox 内的文本。\n\nPCEX 翻译不再覆盖 source：`translate_extraction()` 在 v4 `translations/` 下追加一个完整的\nreplacement-only 层。Markdown/EPUB 随后选择 source、某一译文替换或 source 加某一译文；选择\n依赖 translation ID，未指定时使用 index 第一项。这个渲染选择与直接 EPUB 输入的 append/replace\n提交模式是不同阶段的概念。一键 PDF→Markdown/EPUB 也复用 replacement-only 译文层，显式\n选择本次新生成的 ID；REPLACE / APPEND_TEXT / APPEND_BLOCK 分别映射为 REPLACE /\nBILINGUAL_TEXT / BILINGUAL 渲染。只有临时展示视图组合原译文，不把重复来源写入待校验\nPCEX。BILINGUAL_TEXT 在同一来源 fragment 内追加内容；BILINGUAL 按正文段追加译文。\n资产先按稳定 flow/child 索引组合，再插入双语段落；表格在单元格内组合，不能追加第二张表。PDF Translation Pipeline 也可通过显式 translation ID 选择持久化译文；\n省略 ID 时继续消费根内容。PDF 写回只处理 narrative 与 furniture，不处理缺少字段级来源坐标的\nanchored 图片/表格文字。\n\n`AsyncPDFCraft.convert_pdf_to_markdown()` 和 `AsyncPDFCraft.convert_pdf_to_epub()` 会先提取到内部 workspace，\n再渲染目标输出。提取流程是：\n\n1. 通过 `PDFHandler` 渲染 PDF 页面。\n2. 通过 `OCR.recognize()` 识别页面布局。\n3. 在 `analysing_path/ocr/` 写入 OCR 页 XML 和恢复缓存，在\n   `analysing_path/extraction/assets/` 写入资源。\n4. OCR 的终态页事件经磁盘流恢复输入页序，传统跨页解析与 OCR 并行推进；闭合正文、脚注和\n   未定归属来源存入磁盘数据库。可选 `on_body` / `on_footnote` 交付独立 PCEX XML 快照，\n   回调背压由磁盘队列与 OCR 生产隔离。EOF 排空后再分析 TOC 数据。\n5. 在 `analysing_path/extraction/` 生成章节、TOC、页面几何和 manifest；章节使用 PCEX v3\n   FlowItem，保留文字 fragment、段内 anchored image/table 与独立段落公式的阅读顺序。\n6. 仅从该 extraction 渲染 Markdown 或 EPUB。\n\n章节生成在第 5 步内部先为 TOC 排除后的 OCR Page 建立完整来源记录：保留原文、原分类、\nOCR 流内下标和独立阅读位置。正文沿用原 source_order；新提取的脚注使用正文布局数作为\n偏移，写入同一页的 fragment 身份空间。OCR Page 的 order 不变，旧 PCEX 不重新编号。\n阅读顺序保留正文已有的栏序，将脚注分类中的来源按同栏位置插入，不按全页 y 坐标排序。\n通宽锚点限定阅读带；栏尾来源插在所属栏最后成员之后，不能退回整页或整带末尾。\n传统 join、citation 拆分和同页 ref 匹配只建立初始结论，再投影为 PageAnalysis。\n跨页合并必须把页尾/续页页首 image/table 保留在两侧文字之间，不可挪到整段之后。\nPageAnalysis 的 continuation 表示文字前驱，跳过同一归属流中的图片/表格；资产自身 gap\n保持 false，DisplayFormula 才是硬边界。投影与恢复必须保留段内锚点，精修与校验遵守同一规则。印刷标记的\n完整来源和前缀范围保留到组装：正文投影保留前缀，定义投影才移除前缀。断词修复延后到 ownership\n组装完成后的语义投影；原始文字记录不被改写，投影为空的 fragment 也保留原身份和 bbox。\n\n默认传统算法由 `extractor/chapter/stream.py` 增量连接、来源投影和磁盘组装；无目录页排除时复用\n在线生成的内容数据库，有目录页排除时从 OCR 缓存按排除规则重放。章节定稿逐章处理。\n需要可编辑页级 ownership 的 PageAnalysisProcessor 仍使用 `restore_streams` 的整批事务语义；\n在线回调发生在可选 refinement 之前，不声明是修正后归档的重放。\nFootnoteOptions(refinement=FootnoteRefinement(...)) 在调用方 event loop 在线执行 JEV 筛选及\nLLM 页级修正；CLI 的 --footnotes 只启用传统算法。LLM JSON 通过 request_guaranteed_json\n编译 ownership、必需元数据、引用位置及定义关系，候选在完整暂存对象图上组装成功才接纳。\n候选在暂存对象图通过校验后才提交；允许降级的页保留算法结果，必要时撤销整批精修，任何失败均不修改调用方原对象。不得额外嵌套请求重试。\nref 仍以 layout_id + citation_index + before/mark/after 定位，不要求 LLM 算 offset。\n\n连接的唯一编辑真源为后继的 continues_from_previous；continues_to_next 由此派生，旧字段若\n显式返回，仅作为同页一致性断言。没有按修正先后顺序选择布尔值的 reconciliation。\n已知跨页定义 ID、起点索引与既有跨页片段不可由单页请求删除或重命名。保护同时覆盖 segment\n记录与既有 layout 成员，不能保留 ID 却将成员改为 ignored、正文或其他定义；内部自然段边界仍可编辑。页内非共享定义可以\n通过重分组拆分或合并，新增 ID 为 new-页码-index；未确定归属的续段可明确连接到上一页已知\n定义。无索引段排在本页有索引段前，有索引定义从 1 连续递增。没有起点的已知 continuation\n是非法对象图；unresolved 来源则显式保留为可读正文，不能猜测归属后丢弃。\n\n一条定义可以有零个或多个引用出现；数字、圈号及星号都属于印刷标记域，标记不是定义身份。\n未匹配定义由 Chapter.definitions 保留，经既有 PCEX references 容器编码，无需制造正文 ref。\nMarkdown/EPUB 保留这些定义。正文及脚注 TextFlowItem 共用 narrative_texts 遍历，翻译 coverage\n和 PDF 计划消费同一来源映射；有 coverage 的脚注正文实际回填，无 coverage 的区域仍作障碍。\n多次引用或跨章节复用同一来源不重复回填。定义开头的印刷标记在 PDF 输出时恢复。\n格式专有适配留在 PCEX 层，通用 XML translator 无需了解脚注。\n\nPCEX 格式保持兼容。现有归档和译文层按原身份读取、校验和导出，不做推测重编号；\n缺失 coverage 继续表示 preserved。旧归档若同一章节已有歧义的重复 fragment 身份，仍明确拒绝，\n不声称能够无损迁移。更详细的修复前后证据见 docs/verification/footnote-788.md。\n\n当未传入 `analysing_path` 时，`EnsureFolder` 会创建临时目录。当传入该路径时，它会成为可持久复用的缓存和调试输出目录。\n\n## 中间产物契约\n\nanalysis 与稳定 extraction 明确分离：\n\n- `ocr/page_*.xml`：OCR 页数据。\n- `ocr/done`：表示所有选中页面已完成识别的标记。\n- `plots/`：启用 plot 生成时的可选可视化调试输出。\n- `extraction/manifest.json`：格式版本、producer、创建时间和文档元数据。\n- `extraction/pages.xml`：1-based OCR 像素坐标空间、实际渲染 DPI 和逐页像素宽高。\n- `extraction/assets/`：按内容 hash 存放裁剪出的图片、公式和表格。\n- `extraction/chapters/chapter_*.xml`：生成的 v3 FlowItem 章节记录及原 PDF page/bbox 映射；\n  图表可嵌在 TextFlowItem fragment 之间，DisplayFormula 必须独立且不能被段落拼接跨越。\n- `extraction/toc.xml`、`extraction/cover.png`：可选目录和封面。\n- `extraction/furnitures.xml`：可选的页面家具 pattern 与页级 section。\n- `extraction/translations/index.json`：v4 可选译文索引；条目以 ID 区分，记录目标语言和创建时间。\n- `extraction/translations/<id>/chapters/`、`metadata.json`、`coverage.xml`：一份独立译文层；章节\n  结构与 source 对齐，coverage 记录 Narrative、furniture position/section 及独立图片/表格文本的\n  `translated` / `preserved` 状态。可选 `furnitures.xml` 保存该层的 furniture 译文。\n- `extraction/translation.xml`：旧式或内部临时物化译文视图的根级 coverage；不要把它当作 v4\n  translation layer 索引。\n\nNarrative 翻译中的段内 asset 只以无文本临时 anchor 保持位置；独立 asset 不伪造 anchor。图片/\n表格 title/content/caption 属于独立翻译阶段；标准高层翻译入口固定先运行 NarrativeFlow，随后由\n`with_anchored` 决定是否运行该阶段，并以 anchored coverage 约束 v4 translation layer 中允许变化的 slot。\n\n公共分段流程把 `extraction/` 打包为 `.pcex`；恢复后端只接受 `.pcex` 或已加载的\n`PDFCraftExtraction`。一键转换直接使用 workspace，只有显式 `extraction_path` 时才额外导出\n`.pcex`，避免压缩往返。翻译后的 `.pcex` 必须保留 source 的 manifest、pages、章节、TOC、封面、\nfurniture 和资源，并把每份译文及其 coverage 隔离在对应 translation layer 中。\n`PDFCraftExtraction` 只作为 opaque handle 流转；归档的打开、校验和导出由 `AsyncPDFCraft`\n负责，`PDFCraft` 仅同步转发这些操作。\n\n修改 XML schema、文件命名或跳过语义会影响多个模块，应视为跨流水线变更，并配套有针对性的测试。\n\nMarkdown/EPUB 只从 FlowItem 的阅读顺序渲染，不能也不应复刻固定 PDF 页面几何：Markdown 将\n段内图片/表格安全拆成块级内容；EPUB 默认同样处理，仅在小型图片的相邻 fragment 提供同页、\n另一侧、纵向重叠等强几何证据时尝试可降级的左右 float。表格和 DisplayFormula 不得 float；\n阅读器不支持 CSS 或窄屏时仍必须保留正常块级顺序。\n\n## 重型运行时边界\n\n`PageExtractorNode` 会延迟导入 `doc-page-extractor`，并且只在需要 OCR 时根据 pdf-craft 的 OCR 配置创建上游 extractor。除非任务明确要求 eager loading，否则应保持这种延迟加载行为。\n\nOCR 配置是封闭公共面：`DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`、`DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`。不要把 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 作为 pdf-craft 公共注入口。\n\n本地 OCR 可能需要 Poppler、支持 CUDA 的 PyTorch、大型模型下载和较高显存；供应商 OCR 不需要本地 CUDA，但需要网络和密钥。普通单元测试应保持不依赖这些资源也能运行。\n\n## 错误与恢复语义\n\n失败决策、可信译文缺失与任务级成功的原则见 [失败与有效成果](failure-and-completion.md)。页面级降级会保留图片或明确占位，全部主要工作降级仍须报错。\n\n已存在的 `page_*.xml` 会被跳过。`done` 标记会让 OCR 识别整体跳过。修改恢复行为时要谨慎，因为它同时影响本地手动运行和 VGE worktree 重跑。\n\n## EPUB 原子翻译回调\n\nEPUB 适配器异步逐项读取任务，在共享 XMLTranslator 的局部映射采纳位置接收结果，按章写入\n临时归档；不收集全书正文任务或完整译文结果。同步 iterable 的既有核心接口继续供 PCEX 翻译使用。\n可选 `on_body` 接收 `EpubFragmentContent(epubcfi, original, translated)`，只包含去掉窗口上下文后的\n原子片段；来源 CFI 索引由 EPUB 适配器按需构建，源范围贯穿公式替换。`pipeline/epub/cfi.py`\n只负责文档内定位，OPF/spine 前缀由外层提供。用户说明见 `docs/*/STREAMING.md`。\n\n\n## PCEX 内容流与页面查询\n\n`document/content.py` 逐 XML 内容项解码现有 FlowItem/Reference，body 中引用只携带身份，完整\n定义走独立条目。`document/stream_view.py` 提供固定资源/目录/页面几何与原文/所选层/在线译文流；\n标准 XMLTranslator 不先生成全书译文 workspace。`transformer/content.py` 复用同一引擎，保留\n资源前后文，PCEX writer 是该流的一个消费者。自定义整章 translator 保留显式兼容路径。\n\nMarkdown 逐项写暂存内容，EPUB 逐项转换并 spooling EPUB records，PDF 以来源顺序的磁盘\nfeed 向隔离 Qt 进程提交替换项。PDF 真实布局通过磁盘回传，跨页段落在所有区域排版定稿后\n一次通知；窗口只按现有页级约束收尾，不拿内容回调冒充排版通知。\n\n公共 `document/paragraph.py` 定义 SourceRegion、ParagraphContent、PDFSourceRegion 与\nPDFParagraphContent。翻译/重排渲染使用独立 ParagraphContent（Markdown text、可选 original、\n来源 regions），PDF 使用每区域带字号/行/公式/擦除的 PDFParagraphContent。提取仍保留原有\nXML 回调；EPUB 输入翻译仍保留 EpubFragmentContent。未提供通道时不封装该通道的公共产物。\nSourceRegion.page 复用 PageGeometry，source_order_in_page 对应 PCEX source_order；bbox\n仍为 OCR 像素整数，map_bbox 根据实际页面尺寸返回 float。render_dpi 是请求值，不能单独\n作为缩放依据。PDF 公共布局的坐标与字号也换算为来源 OCR 像素，不引入 font_id。\n\n`document/access.py` 的 read_pages/read_asset 与 ExtractionContext 继续作为公共几何/资源\n访问入口。内容对象包含页面几何，因此回调消费者不必再做页面联表。资源只通过受支持的\n字节读取入口获取，不依赖临时目录。\n\nLLM 有序交付使用磁盘重排缓存；异步任务上下文、来源 DOM 与完成结果都可落盘，通过\n稳定 root/node identity 在当前文档交付时恢复映射，队首慢请求不再保留全书原文 DOM。\n同步 iterable/自定义整章契约保留原对象身份。双阶段 vendor OCR 续阶段优先，未确定归属来源\n按阅读顺序发出，延续 #794/#795 回归。\n\n\n元信息通知复用 `document/metadata.py` 的 MetadataContent/DocumentMetadata。\nPDF 原生-only 通知在 OCR 前，LLM 提取通知在字段合并后；异常不能被提取降级逻辑吞掉。\nPCEX 的共享 NarrativeXMLTransformer 元信息阶段负责翻译通知，EPUB 适配器负责 OPF 视图通知；\n书籍通知不包含 TOC 或文档 head/title。只有用户提供 on_metadata 时才构造独立公共快照。\nEpubTranslation 仅作为 EpubFragmentContent 的兼容别名。\n","AGENTS.md":"# Agent 工作流\n\npdf-craft 是一个把扫描书籍 PDF 转换为 Markdown 或 EPUB 的 Python 库。本仓库使用 `~/.agents/skills/vibecoding` 作为通用维护工作法。本文件只记录 pdf-craft 特有的边界和按需阅读路由。\n\n## 工作区边界\n\n- `pdf_craft/` 是包源码。公共导入从 `pdf_craft/__init__.py` 暴露，便利入口在 `pdf_craft/functions.py`。\n- `tests/` 包含轻量单元测试和小型 PDF fixture。普通代码改动默认以这些测试作为验证面。\n- `docs/` 和 `README.md` 是读者/贡献者阅读的文档。不要把这些说明复制到 Agent 文档里。\n- `references/` 是 Agent 面向的引用文档。只阅读当前任务需要的引用文档。\n- `pdf_craft_tool/` 是未发布的本地 CLI，承载手动转换、翻译和冒烟矩阵；`scripts/` 只保留依赖源码同步辅助脚本。不要把它们当作默认开发流程。\n- `analysing/`、`pdf-craft-output/`、`models-cache/`、`.venv/`、`dist/`、`build/` 和 `*.egg-info` 是生成产物或本地运行产物。\n\n## 仅在需要时阅读\n\n- 当需要判断模块归属、公共 API 边界或新代码应放在哪里时，阅读[架构与模块边界](references/architecture.md)。\n- 当修改 PDF 提取、OCR 归一化、缓存 XML 产物、目录生成、章节生成、Markdown 渲染或 EPUB 渲染时，阅读[转换流水线](references/conversion-pipeline.md)。\n- 当修改 PDF 回填、QTextLayout、字号拟合、行数/单行规则或 PDF 文本层排版时，阅读[两阶段 PDF 文本排版](references/pdf-text-layout.md)。\n- 当选择 setup、验证、worktree 行为、发布或外部依赖处理方式时，阅读[开发与 Worktree](references/development-and-worktrees.md)。\n- 当准备发版、更新版本号、编写 changelog 或调整发布流程时，阅读[发版流程](references/release-workflow.md)。\n\n- 当修改失败恢复、翻译缺失、错误定位或任务成功判断时，阅读[失败与有效成果](references/failure-and-completion.md)。\n\n## 项目特有默认规则\n\n- 这是库项目。除非未来任务引入长期服务，否则不要启动常驻开发服务器。\n- 普通验证应避免 CUDA、模型下载、网络请求和完整 PDF 转换，除非任务明确触及这些行为。\n- 模型缓存和转换输出不得进入提交内容。`pdf_craft_tool` 的产物默认写入每个 worktree 自己的 `pdf-craft-output/`；只有任务明确需要 OCR 时，才考虑共享外部模型缓存。\n- 纯文档任务不得修改包代码或依赖版本。\n","references/development-and-worktrees.md":"# 开发与 Worktree\n\n**约束范围：** setup、验证、VGE worktree 行为和发布命令。**不约束：** 包架构或转换内部逻辑。**何时阅读：** 选择命令、编辑项目配置或修改开发文档时。\n\n## 环境\n\n使用 Python `>=3.11,<3.14`；默认开发版本是 Python 3.11。本仓库使用 Poetry 2.x；配置 `poetry config virtualenvs.in-project true` 时，虚拟环境应位于当前 worktree 内。\n\n普通开发安装依赖：\n\n```bash\npoetry config virtualenvs.in-project true\npoetry install --with dev\n```\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不直接声明 `torch` 或 `torchvision`。只有当任务需要指定 CPU 或 CUDA wheel 时，才按当前环境覆盖安装 `torch` 和 `torchvision`。\n\n真实 OCR 有六种配置入口：\n\n- `DeepSeekOCRLocalConfig`：本地 DeepSeek OCR，真实转换需要 CUDA。\n- `DeepSeekOCR2LocalConfig`：本地 DeepSeek OCR 2，真实转换需要 CUDA。\n- `UnlimitedOCRLocalConfig`：本地 Unlimited OCR，真实转换需要 CUDA。\n- `DeepSeekOCRVendorConfig`：DeepSeek OCR 供应商模式。\n- `DeepSeekOCR2VendorConfig`：DeepSeek OCR 2 供应商模式。\n- `UnlimitedOCRVendorConfig`：Unlimited OCR 供应商模式。\n\n库代码不得自动读取 `.env`。未发布的 `pdf_craft_tool` CLI 通过 `pdf_craft_tool/runtime.py` 加载工作区 `.env` 并创建显式配置对象。所有手动运行时变量使用 `PDF_CRAFT_` 前缀：OCR 使用 `PDF_CRAFT_OCR_MODE` 和对应的 `PDF_CRAFT_*_OCR_*` 配置；文本 LLM 通过可复用的 `PDF_CRAFT_LLM_<PROFILE>_*` profile 配置。默认 profile 可在运行时从 `oo llm config --json` 获取 OOMOL 凭据，不能复用 OCR-only endpoint。\n\n## 默认验证\n\n优先选择能跨越改动边界的最小命令集：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\n```\n\n`poetry build` 用于验证打包。`scripts/` 中的完整转换脚本是手动检查，可能需要 Poppler、PyTorch、模型缓存和 CUDA。\n\n## VGE Worktree 行为\n\n本项目没有长期运行的开发服务，因此 `.conductor/settings.toml` 不应定义 `scripts.run`，除非未来项目新增 watcher 或 server。VGE setup 同时负责安装依赖并初始化当前 worktree 的 `.env`，确保当前 worktree 中的 pdf-craft 及其 `doc-page-extractor` 依赖可以被导入和测试。\n\n`.env` 是 worktree 私有运行配置，不提交到 Git。VGE setup 在当前 worktree 缺少 `.env` 时，应优先从源工作区的 `.env` 复制，以便继承供应商 OCR 密钥和本机开发配置；只有源工作区也没有 `.env` 时，才从 `.env.template` 创建空配置。\n\n默认不配置 `scripts.archive`。VGE 会负责释放 worktree；项目级 Cleanup 只适合需要保留现场后执行额外归档或收尾动作的项目，本仓库当前没有这种需求。\n\n`pdf_craft_tool` 生成的转换输出默认留在当前 worktree 的 Git 忽略目录 `pdf-craft-output/` 下。模型缓存可能很大；只有在有意进行本地 OCR 工作时才使用 `models-cache/`，不要提交它，也不要假设其中已有内容。\n\n## 人类阅读文档\n\n面向人类贡献者的开发说明位于 `docs/DEVELOPMENT.md` 和 `docs/DEVELOPMENT_zh-CN.md`。README 文件应聚焦库使用者。Agent 面向的路由和项目约束保留在 `AGENTS.md` 和 `references/` 中。\n\n## 发布\n\n发版准备流程位于 [release-workflow.md](release-workflow.md)，各版本发布说明位于 `docs/changelog/`。除非任务明确要求发布或升级依赖，否则普通 worktree 支持改造不应修改包版本、依赖 pin 或发布元数据。\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/oomol-lab/pdf-craft/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent 工作流\n\npdf-craft 是一个把扫描书籍 PDF 转换为 Markdown 或 EPUB 的 Python 库。本仓库使用 `~/.agents/skills/vibecoding` 作为通用维护工作法。本文件只记录 pdf-craft 特有的边界和按需阅读路由。\n\n## 工作区边界\n\n- `pdf_craft/` 是包源码。公共导入从 `pdf_craft/__init__.py` 暴露，便利入口在 `pdf_craft/functions.py`。\n- `tests/` 包含轻量单元测试和小型 PDF fixture。普通代码改动默认以这些测试作为验证面。\n- `docs/` 和 `README.md` 是读者/贡献者阅读的文档。不要把这些说明复制到 Agent 文档里。\n- `references/` 是 Agent 面向的引用文档。只阅读当前任务需要的引用文档。\n- `pdf_craft_tool/` 是未发布的本地 CLI，承载手动转换、翻译和冒烟矩阵；`scripts/` 只保留依赖源码同步辅助脚本。不要把它们当作默认开发流程。\n- `analysing/`、`pdf-craft-output/`、`models-cache/`、`.venv/`、`dist/`、`build/` 和 `*.egg-info` 是生成产物或本地运行产物。\n\n## 仅在需要时阅读\n\n- 当需要判断模块归属、公共 API 边界或新代码应放在哪里时，阅读[架构与模块边界](references/architecture.md)。\n- 当修改 PDF 提取、OCR 归一化、缓存 XML 产物、目录生成、章节生成、Markdown 渲染或 EPUB 渲染时，阅读[转换流水线](references/conversion-pipeline.md)。\n- 当修改 PDF 回填、QTextLayout、字号拟合、行数/单行规则或 PDF 文本层排版时，阅读[两阶段 PDF 文本排版](references/pdf-text-layout.md)。\n- 当选择 setup、验证、worktree 行为、发布或外部依赖处理方式时，阅读[开发与 Worktree](references/development-and-worktrees.md)。\n- 当准备发版、更新版本号、编写 changelog 或调整发布流程时，阅读[发版流程](references/release-workflow.md)。\n\n- 当修改失败恢复、翻译缺失、错误定位或任务成功判断时，阅读[失败与有效成果](references/failure-and-completion.md)。\n\n## 项目特有默认规则\n\n- 这是库项目。除非未来任务引入长期服务，否则不要启动常驻开发服务器。\n- 普通验证应避免 CUDA、模型下载、网络请求和完整 PDF 转换，除非任务明确触及这些行为。\n- 模型缓存和转换输出不得进入提交内容。`pdf_craft_tool` 的产物默认写入每个 worktree 自己的 `pdf-craft-output/`；只有任务明确需要 OCR 时，才考虑共享外部模型缓存。\n- 纯文档任务不得修改包代码或依赖版本。\n","isInternal":false,"tokens":873,"sizeBytes":2552},{"name":"architecture.md","path":"references/architecture.md","rawUrl":"https://raw.githubusercontent.com/oomol-lab/pdf-craft/HEAD/references/architecture.md","title":"references - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# 架构与模块边界\n\n**约束范围：** 包结构、公共 API 和模块归属。**不约束：** OCR 算法细节、开发命令或发布步骤。**何时阅读：** 判断代码应放在哪里，或修改公共导入面时。\n\n## 包公共面\n\n## 新模块边界\n\n`pdf_craft` 的主要业务模块围绕可组合的文档处理阶段组织：\n\n- `extractor/`：PDF 页面、OCR、目录和章节分析的适配入口；产出 PDFCraftExtraction。\n- `document/`：PDFCraftExtraction 的 workspace/`.pcex` 存储、校验和来源位置契约。\n- `renderer/`：PDFCraftExtraction 到 Markdown 或 EPUB 的格式渲染入口。\n- `transformer/`：格式无关的 XML 内容变换，包括 LLM 翻译、结构填充和校验。\n- `pipeline/`：格式专属编排。EPUB Pipeline 将 EPUB XHTML/目录/元数据交给 Transformer；PDF Pipeline 以 replace-only 方式将 Chapter 来源区域写回 PDF。\n\n公开中间格式为 `PDFCraftExtraction`，持久化与交换载体必须是 `.pcex` ZIP。其内容为\n`manifest.json`、`pages.xml`、`chapters/`、`assets/`，以及可选 `toc.xml`、`cover.png`、\n`furnitures.xml` 和 v4 `translations/`。`translations/index.json` 按文件内唯一 ID 排列零到多个\n译文层；每层包含完整章节译文、metadata overlay、`coverage.xml`，以及可选 furniture 译文。\n目标语言只是层的元数据，同一种语言可以有多个 ID。根级 `translation.xml` 仍是旧式或内部物化\n译文视图的覆盖记录，不代表 v4 中可供渲染选择的持久化译文层。\n\nPCEX 格式版本以已经发布的 Git tag 为兼容基线，不随开发期间的中间 commit 逐次递增。只有准备\n新版本且格式相对上一个已发布版本发生 schema 变化时，才评估是否增加 `format_version`。当前 v4\n相对 `v2.3.1` 的 v3 增加了可叠加 translation layer；读取器继续接受 v1/v2/v3。\n\nPCEX chapter 的阅读流使用 `FlowItem`：`TextFlowItem` 是作者意义的正文/标题段，包含保留\n来源 bbox 的 `SourceTextFragment`，并可在 fragment 之间嵌入图片/表格 `SourceAsset`。\n`DisplayFormula` 与 `StandaloneAsset` 是独立 flow node；段落公式不是 AnchoredContent，必须\n阻断段落拼接。类名与 chapter XML 元素一一对应，详见 `docs/*/PCEX_FORMAT.md`。\nNarrative 翻译仅把嵌在 `TextFlowItem` 中的图片/表格替换为无文本的临时 anchor，不能读取或覆写\nasset 文本；`StandaloneAsset` 不伪造 paragraph anchor，并与 NarrativeFlow 隔离；\n`AnchoredContentTransformer` 是另一条按 asset 小批次工作的翻译边界，负责 title/content/caption。\n目录-backed 形态只供一键转换在 `analysing_path/extraction/` 内部衔接前后端；普通目录不是\n公开输入。`ocr/`、`plots/`、`done` 和其他 analysis 文件仅是可丢弃的诊断/恢复缓存。\n\n`pdf_craft/__init__.py` 是公共导入面。`AsyncPDFCraft` 是业务核心，`PDFCraft` 是位于\n`pdf_craft/sync/` 的同步门面；`pdf_craft/transform.py` 是 PDF 前端\n提取 engine。Markdown、EPUB、翻译和 PDF 写回后端只能读取 PDFCraftExtraction，不得读取\nanalysis/OCR 缓存。\n\n`PDFCraftExtraction` 是公开但不包含业务方法的 opaque handle。用户只能通过两个 Craft 门面的\n`open_extraction` / `export_extraction` 打开和导出，再把句柄传回门面或高级组件。高级可组合组件\n（Extractor、Renderer、Transformer、LLM runtime）仅提供无 `_async` 后缀的异步方法；同步适配\n不得进入业务核心。配置、选项、事件和数据类不拆分同步/异步版本。模型下载同样以 async 实现为准，\n同步使用通过 `PDFCraft.predownload_models()` 委派。\n\n业务执行以 async 为唯一真源：请求、重试、repair loop、编排和转换不能再提供第二套同步实现。\n`run_sync` 只能由 `pdf_craft/sync/` 的用户门面调用；禁止在实现层启动事件循环把同步业务伪装成\n异步业务。用户提供的 LLM、Transformer、TOC 转换、PDF handler 和图片 URL resolver 执行方法\n必须是异步契约，不提供同步扩展适配。普通事件通知可以接受同步或异步 callback。\n仅当第三方库没有异步接口时，才在独立执行域中调用同步适配器；适配器只承担第三方调用、\n文件读写和必要资源清理，不复制业务编排。跨域 callback 桥接不得成为另一条请求或转换路径。\n\n`translate_extraction()` 保留 source layer 并追加 replacement-only 译文层。Markdown/EPUB 渲染\n通过 `RenderMode.SOURCE`、`REPLACE`、`BILINGUAL` 决定只读原文、选择一份译文或合并原译文；\n`BILINGUAL_TEXT` 在同一来源 fragment 内追加译文。这些译文模式可显式传 translation ID，省略时稳定选择 index 第一项。PDF 写回也可显式选择译文层，\n但仅消费已有正文和 furniture 写回能力，不写回 anchored 图片/表格文字。\n\n除非任务明确要求破坏性 API 变更，否则把以下名称和默认值视为公共 API：\n\n- `PDFCraft`、`AsyncPDFCraft`、`PDFOptions`、`ExtractionOptions`、`PDFCraftExtraction`\n- `PDFExtractor`、`MarkdownRenderer`、`EpubRenderer`\n- `ExtractionTransformer`、`NarrativeXMLTransformer`\n- `AnchoredContentExtractionTransformer`、`AnchoredContentTransformer`、`AnchoredContentXMLTransformer`\n- `predownload_models`\n- `LLM`\n- `JEV`、`FootnoteOptions`、`FootnoteRefinement`\n- `DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`\n- `DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`\n- `AsyncPDFHandler`、`AsyncPDFDocument`\n- `BookMeta`、`TableRender`、`LaTeXRender`\n\n## 模块归属\n\n- `pdf_craft/pdf/` 负责 PDF 元数据、渲染、页引用、通过 `doc-page-extractor` 接入 OCR 后端，以及 OCR 页 XML 数据。\n- `pdf_craft/extractor/toc/` 负责目录页检测和标题层级分析，包括可选的 LLM 辅助分析。\n- `pdf_craft/extractor/chapter/` 负责根据 OCR 页 XML 和 TOC 事实生成章节结构。\n- `pdf_craft/markdown/` 负责 Markdown 段落解析和 Markdown 输出渲染。\n- `pdf_craft/renderer/epub/` 负责把章节数据转换为 `epub-generator` 的记录并生成 EPUB。\n- `pdf_craft/llm/` 负责增强目录分析所需的可选 LLM 调用。核心转换应保持不依赖该增强能力也可使用。\n- `pdf_craft/common/` 负责可复用的文件系统、XML、资源和统计辅助逻辑。\n\n## 外部包边界\n\n`doc-page-extractor` 和 `epub-generator` 是被 pin 住的运行时依赖。它们内部的问题通常应在各自仓库修复，再通过版本升级或明确的本地联调引入。本仓库 `scripts/` 下的同步脚本会覆盖 `.venv` 中已安装的依赖源码；这些脚本只是手动本地联调辅助，不是普通项目 setup。\n\npdf-craft 对外只暴露自己的 OCR 配置对象，不暴露 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 注入口。需要新增 OCR 后端时，优先在 `doc-page-extractor` 增加官方构造入口，再在 pdf-craft 映射成封闭配置对象。\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不要把 `torch` 或 `torchvision` 作为 pdf-craft 的直接运行时依赖；用户仍可能需要按自己的环境覆盖安装 CPU 或 CUDA wheel。\n","isInternal":false,"tokens":2515,"sizeBytes":7359},{"name":"conversion-pipeline.md","path":"references/conversion-pipeline.md","rawUrl":"https://raw.githubusercontent.com/oomol-lab/pdf-craft/HEAD/references/conversion-pipeline.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# 转换流水线\n\n**约束范围：** PDF 到输出文件的流程、中间产物和转换契约。**不约束：** 通用 setup 或打包。**何时阅读：** 修改提取、TOC、章节生成、Markdown 渲染或 EPUB 渲染时。\n\n## 运行时流程\n\n## 组合流程\n\nExtractor 生成 PDFCraftExtraction 后，Renderer 可直接生成 Markdown 或 EPUB。可选 Transformer\n可以在渲染前修改结构化文本；`pipeline/epub` 也可把既有 EPUB 的 XHTML、目录和元数据交给\n同一个 XML Transformer。PDF Translation Pipeline 只处理已记录来源 bbox 内的文本。\n\nPCEX 翻译不再覆盖 source：`translate_extraction()` 在 v4 `translations/` 下追加一个完整的\nreplacement-only 层。Markdown/EPUB 随后选择 source、某一译文替换或 source 加某一译文；选择\n依赖 translation ID，未指定时使用 index 第一项。这个渲染选择与直接 EPUB 输入的 append/replace\n提交模式是不同阶段的概念。一键 PDF→Markdown/EPUB 也复用 replacement-only 译文层，显式\n选择本次新生成的 ID；REPLACE / APPEND_TEXT / APPEND_BLOCK 分别映射为 REPLACE /\nBILINGUAL_TEXT / BILINGUAL 渲染。只有临时展示视图组合原译文，不把重复来源写入待校验\nPCEX。BILINGUAL_TEXT 在同一来源 fragment 内追加内容；BILINGUAL 按正文段追加译文。\n资产先按稳定 flow/child 索引组合，再插入双语段落；表格在单元格内组合，不能追加第二张表。PDF Translation Pipeline 也可通过显式 translation ID 选择持久化译文；\n省略 ID 时继续消费根内容。PDF 写回只处理 narrative 与 furniture，不处理缺少字段级来源坐标的\nanchored 图片/表格文字。\n\n`AsyncPDFCraft.convert_pdf_to_markdown()` 和 `AsyncPDFCraft.convert_pdf_to_epub()` 会先提取到内部 workspace，\n再渲染目标输出。提取流程是：\n\n1. 通过 `PDFHandler` 渲染 PDF 页面。\n2. 通过 `OCR.recognize()` 识别页面布局。\n3. 在 `analysing_path/ocr/` 写入 OCR 页 XML 和恢复缓存，在\n   `analysing_path/extraction/assets/` 写入资源。\n4. OCR 的终态页事件经磁盘流恢复输入页序，传统跨页解析与 OCR 并行推进；闭合正文、脚注和\n   未定归属来源存入磁盘数据库。可选 `on_body` / `on_footnote` 交付独立 PCEX XML 快照，\n   回调背压由磁盘队列与 OCR 生产隔离。EOF 排空后再分析 TOC 数据。\n5. 在 `analysing_path/extraction/` 生成章节、TOC、页面几何和 manifest；章节使用 PCEX v3\n   FlowItem，保留文字 fragment、段内 anchored image/table 与独立段落公式的阅读顺序。\n6. 仅从该 extraction 渲染 Markdown 或 EPUB。\n\n章节生成在第 5 步内部先为 TOC 排除后的 OCR Page 建立完整来源记录：保留原文、原分类、\nOCR 流内下标和独立阅读位置。正文沿用原 source_order；新提取的脚注使用正文布局数作为\n偏移，写入同一页的 fragment 身份空间。OCR Page 的 order 不变，旧 PCEX 不重新编号。\n阅读顺序保留正文已有的栏序，将脚注分类中的来源按同栏位置插入，不按全页 y 坐标排序。\n通宽锚点限定阅读带；栏尾来源插在所属栏最后成员之后，不能退回整页或整带末尾。\n传统 join、citation 拆分和同页 ref 匹配只建立初始结论，再投影为 PageAnalysis。\n跨页合并必须把页尾/续页页首 image/table 保留在两侧文字之间，不可挪到整段之后。\nPageAnalysis 的 continuation 表示文字前驱，跳过同一归属流中的图片/表格；资产自身 gap\n保持 false，DisplayFormula 才是硬边界。投影与恢复必须保留段内锚点，精修与校验遵守同一规则。印刷标记的\n完整来源和前缀范围保留到组装：正文投影保留前缀，定义投影才移除前缀。断词修复延后到 ownership\n组装完成后的语义投影；原始文字记录不被改写，投影为空的 fragment 也保留原身份和 bbox。\n\n默认传统算法由 `extractor/chapter/stream.py` 增量连接、来源投影和磁盘组装；无目录页排除时复用\n在线生成的内容数据库，有目录页排除时从 OCR 缓存按排除规则重放。章节定稿逐章处理。\n需要可编辑页级 ownership 的 PageAnalysisProcessor 仍使用 `restore_streams` 的整批事务语义；\n在线回调发生在可选 refinement 之前，不声明是修正后归档的重放。\nFootnoteOptions(refinement=FootnoteRefinement(...)) 在调用方 event loop 在线执行 JEV 筛选及\nLLM 页级修正；CLI 的 --footnotes 只启用传统算法。LLM JSON 通过 request_guaranteed_json\n编译 ownership、必需元数据、引用位置及定义关系，候选在完整暂存对象图上组装成功才接纳。\n候选在暂存对象图通过校验后才提交；允许降级的页保留算法结果，必要时撤销整批精修，任何失败均不修改调用方原对象。不得额外嵌套请求重试。\nref 仍以 layout_id + citation_index + before/mark/after 定位，不要求 LLM 算 offset。\n\n连接的唯一编辑真源为后继的 continues_from_previous；continues_to_next 由此派生，旧字段若\n显式返回，仅作为同页一致性断言。没有按修正先后顺序选择布尔值的 reconciliation。\n已知跨页定义 ID、起点索引与既有跨页片段不可由单页请求删除或重命名。保护同时覆盖 segment\n记录与既有 layout 成员，不能保留 ID 却将成员改为 ignored、正文或其他定义；内部自然段边界仍可编辑。页内非共享定义可以\n通过重分组拆分或合并，新增 ID 为 new-页码-index；未确定归属的续段可明确连接到上一页已知\n定义。无索引段排在本页有索引段前，有索引定义从 1 连续递增。没有起点的已知 continuation\n是非法对象图；unresolved 来源则显式保留为可读正文，不能猜测归属后丢弃。\n\n一条定义可以有零个或多个引用出现；数字、圈号及星号都属于印刷标记域，标记不是定义身份。\n未匹配定义由 Chapter.definitions 保留，经既有 PCEX references 容器编码，无需制造正文 ref。\nMarkdown/EPUB 保留这些定义。正文及脚注 TextFlowItem 共用 narrative_texts 遍历，翻译 coverage\n和 PDF 计划消费同一来源映射；有 coverage 的脚注正文实际回填，无 coverage 的区域仍作障碍。\n多次引用或跨章节复用同一来源不重复回填。定义开头的印刷标记在 PDF 输出时恢复。\n格式专有适配留在 PCEX 层，通用 XML translator 无需了解脚注。\n\nPCEX 格式保持兼容。现有归档和译文层按原身份读取、校验和导出，不做推测重编号；\n缺失 coverage 继续表示 preserved。旧归档若同一章节已有歧义的重复 fragment 身份，仍明确拒绝，\n不声称能够无损迁移。更详细的修复前后证据见 docs/verification/footnote-788.md。\n\n当未传入 `analysing_path` 时，`EnsureFolder` 会创建临时目录。当传入该路径时，它会成为可持久复用的缓存和调试输出目录。\n\n## 中间产物契约\n\nanalysis 与稳定 extraction 明确分离：\n\n- `ocr/page_*.xml`：OCR 页数据。\n- `ocr/done`：表示所有选中页面已完成识别的标记。\n- `plots/`：启用 plot 生成时的可选可视化调试输出。\n- `extraction/manifest.json`：格式版本、producer、创建时间和文档元数据。\n- `extraction/pages.xml`：1-based OCR 像素坐标空间、实际渲染 DPI 和逐页像素宽高。\n- `extraction/assets/`：按内容 hash 存放裁剪出的图片、公式和表格。\n- `extraction/chapters/chapter_*.xml`：生成的 v3 FlowItem 章节记录及原 PDF page/bbox 映射；\n  图表可嵌在 TextFlowItem fragment 之间，DisplayFormula 必须独立且不能被段落拼接跨越。\n- `extraction/toc.xml`、`extraction/cover.png`：可选目录和封面。\n- `extraction/furnitures.xml`：可选的页面家具 pattern 与页级 section。\n- `extraction/translations/index.json`：v4 可选译文索引；条目以 ID 区分，记录目标语言和创建时间。\n- `extraction/translations/<id>/chapters/`、`metadata.json`、`coverage.xml`：一份独立译文层；章节\n  结构与 source 对齐，coverage 记录 Narrative、furniture position/section 及独立图片/表格文本的\n  `translated` / `preserved` 状态。可选 `furnitures.xml` 保存该层的 furniture 译文。\n- `extraction/translation.xml`：旧式或内部临时物化译文视图的根级 coverage；不要把它当作 v4\n  translation layer 索引。\n\nNarrative 翻译中的段内 asset 只以无文本临时 anchor 保持位置；独立 asset 不伪造 anchor。图片/\n表格 title/content/caption 属于独立翻译阶段；标准高层翻译入口固定先运行 NarrativeFlow，随后由\n`with_anchored` 决定是否运行该阶段，并以 anchored coverage 约束 v4 translation layer 中允许变化的 slot。\n\n公共分段流程把 `extraction/` 打包为 `.pcex`；恢复后端只接受 `.pcex` 或已加载的\n`PDFCraftExtraction`。一键转换直接使用 workspace，只有显式 `extraction_path` 时才额外导出\n`.pcex`，避免压缩往返。翻译后的 `.pcex` 必须保留 source 的 manifest、pages、章节、TOC、封面、\nfurniture 和资源，并把每份译文及其 coverage 隔离在对应 translation layer 中。\n`PDFCraftExtraction` 只作为 opaque handle 流转；归档的打开、校验和导出由 `AsyncPDFCraft`\n负责，`PDFCraft` 仅同步转发这些操作。\n\n修改 XML schema、文件命名或跳过语义会影响多个模块，应视为跨流水线变更，并配套有针对性的测试。\n\nMarkdown/EPUB 只从 FlowItem 的阅读顺序渲染，不能也不应复刻固定 PDF 页面几何：Markdown 将\n段内图片/表格安全拆成块级内容；EPUB 默认同样处理，仅在小型图片的相邻 fragment 提供同页、\n另一侧、纵向重叠等强几何证据时尝试可降级的左右 float。表格和 DisplayFormula 不得 float；\n阅读器不支持 CSS 或窄屏时仍必须保留正常块级顺序。\n\n## 重型运行时边界\n\n`PageExtractorNode` 会延迟导入 `doc-page-extractor`，并且只在需要 OCR 时根据 pdf-craft 的 OCR 配置创建上游 extractor。除非任务明确要求 eager loading，否则应保持这种延迟加载行为。\n\nOCR 配置是封闭公共面：`DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`、`DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`。不要把 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 作为 pdf-craft 公共注入口。\n\n本地 OCR 可能需要 Poppler、支持 CUDA 的 PyTorch、大型模型下载和较高显存；供应商 OCR 不需要本地 CUDA，但需要网络和密钥。普通单元测试应保持不依赖这些资源也能运行。\n\n## 错误与恢复语义\n\n失败决策、可信译文缺失与任务级成功的原则见 [失败与有效成果](failure-and-completion.md)。页面级降级会保留图片或明确占位，全部主要工作降级仍须报错。\n\n已存在的 `page_*.xml` 会被跳过。`done` 标记会让 OCR 识别整体跳过。修改恢复行为时要谨慎，因为它同时影响本地手动运行和 VGE worktree 重跑。\n\n## EPUB 原子翻译回调\n\nEPUB 适配器异步逐项读取任务，在共享 XMLTranslator 的局部映射采纳位置接收结果，按章写入\n临时归档；不收集全书正文任务或完整译文结果。同步 iterable 的既有核心接口继续供 PCEX 翻译使用。\n可选 `on_body` 接收 `EpubFragmentContent(epubcfi, original, translated)`，只包含去掉窗口上下文后的\n原子片段；来源 CFI 索引由 EPUB 适配器按需构建，源范围贯穿公式替换。`pipeline/epub/cfi.py`\n只负责文档内定位，OPF/spine 前缀由外层提供。用户说明见 `docs/*/STREAMING.md`。\n\n\n## PCEX 内容流与页面查询\n\n`document/content.py` 逐 XML 内容项解码现有 FlowItem/Reference，body 中引用只携带身份，完整\n定义走独立条目。`document/stream_view.py` 提供固定资源/目录/页面几何与原文/所选层/在线译文流；\n标准 XMLTranslator 不先生成全书译文 workspace。`transformer/content.py` 复用同一引擎，保留\n资源前后文，PCEX writer 是该流的一个消费者。自定义整章 translator 保留显式兼容路径。\n\nMarkdown 逐项写暂存内容，EPUB 逐项转换并 spooling EPUB records，PDF 以来源顺序的磁盘\nfeed 向隔离 Qt 进程提交替换项。PDF 真实布局通过磁盘回传，跨页段落在所有区域排版定稿后\n一次通知；窗口只按现有页级约束收尾，不拿内容回调冒充排版通知。\n\n公共 `document/paragraph.py` 定义 SourceRegion、ParagraphContent、PDFSourceRegion 与\nPDFParagraphContent。翻译/重排渲染使用独立 ParagraphContent（Markdown text、可选 original、\n来源 regions），PDF 使用每区域带字号/行/公式/擦除的 PDFParagraphContent。提取仍保留原有\nXML 回调；EPUB 输入翻译仍保留 EpubFragmentContent。未提供通道时不封装该通道的公共产物。\nSourceRegion.page 复用 PageGeometry，source_order_in_page 对应 PCEX source_order；bbox\n仍为 OCR 像素整数，map_bbox 根据实际页面尺寸返回 float。render_dpi 是请求值，不能单独\n作为缩放依据。PDF 公共布局的坐标与字号也换算为来源 OCR 像素，不引入 font_id。\n\n`document/access.py` 的 read_pages/read_asset 与 ExtractionContext 继续作为公共几何/资源\n访问入口。内容对象包含页面几何，因此回调消费者不必再做页面联表。资源只通过受支持的\n字节读取入口获取，不依赖临时目录。\n\nLLM 有序交付使用磁盘重排缓存；异步任务上下文、来源 DOM 与完成结果都可落盘，通过\n稳定 root/node identity 在当前文档交付时恢复映射，队首慢请求不再保留全书原文 DOM。\n同步 iterable/自定义整章契约保留原对象身份。双阶段 vendor OCR 续阶段优先，未确定归属来源\n按阅读顺序发出，延续 #794/#795 回归。\n\n\n元信息通知复用 `document/metadata.py` 的 MetadataContent/DocumentMetadata。\nPDF 原生-only 通知在 OCR 前，LLM 提取通知在字段合并后；异常不能被提取降级逻辑吞掉。\nPCEX 的共享 NarrativeXMLTransformer 元信息阶段负责翻译通知，EPUB 适配器负责 OPF 视图通知；\n书籍通知不包含 TOC 或文档 head/title。只有用户提供 on_metadata 时才构造独立公共快照。\nEpubTranslation 仅作为 EpubFragmentContent 的兼容别名。\n","isInternal":false,"tokens":4986,"sizeBytes":14690},{"name":"development-and-worktrees.md","path":"references/development-and-worktrees.md","rawUrl":"https://raw.githubusercontent.com/oomol-lab/pdf-craft/HEAD/references/development-and-worktrees.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# 开发与 Worktree\n\n**约束范围：** setup、验证、VGE worktree 行为和发布命令。**不约束：** 包架构或转换内部逻辑。**何时阅读：** 选择命令、编辑项目配置或修改开发文档时。\n\n## 环境\n\n使用 Python `>=3.11,<3.14`；默认开发版本是 Python 3.11。本仓库使用 Poetry 2.x；配置 `poetry config virtualenvs.in-project true` 时，虚拟环境应位于当前 worktree 内。\n\n普通开发安装依赖：\n\n```bash\npoetry config virtualenvs.in-project true\npoetry install --with dev\n```\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不直接声明 `torch` 或 `torchvision`。只有当任务需要指定 CPU 或 CUDA wheel 时，才按当前环境覆盖安装 `torch` 和 `torchvision`。\n\n真实 OCR 有六种配置入口：\n\n- `DeepSeekOCRLocalConfig`：本地 DeepSeek OCR，真实转换需要 CUDA。\n- `DeepSeekOCR2LocalConfig`：本地 DeepSeek OCR 2，真实转换需要 CUDA。\n- `UnlimitedOCRLocalConfig`：本地 Unlimited OCR，真实转换需要 CUDA。\n- `DeepSeekOCRVendorConfig`：DeepSeek OCR 供应商模式。\n- `DeepSeekOCR2VendorConfig`：DeepSeek OCR 2 供应商模式。\n- `UnlimitedOCRVendorConfig`：Unlimited OCR 供应商模式。\n\n库代码不得自动读取 `.env`。未发布的 `pdf_craft_tool` CLI 通过 `pdf_craft_tool/runtime.py` 加载工作区 `.env` 并创建显式配置对象。所有手动运行时变量使用 `PDF_CRAFT_` 前缀：OCR 使用 `PDF_CRAFT_OCR_MODE` 和对应的 `PDF_CRAFT_*_OCR_*` 配置；文本 LLM 通过可复用的 `PDF_CRAFT_LLM_<PROFILE>_*` profile 配置。默认 profile 可在运行时从 `oo llm config --json` 获取 OOMOL 凭据，不能复用 OCR-only endpoint。\n\n## 默认验证\n\n优先选择能跨越改动边界的最小命令集：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\n```\n\n`poetry build` 用于验证打包。`scripts/` 中的完整转换脚本是手动检查，可能需要 Poppler、PyTorch、模型缓存和 CUDA。\n\n## VGE Worktree 行为\n\n本项目没有长期运行的开发服务，因此 `.conductor/settings.toml` 不应定义 `scripts.run`，除非未来项目新增 watcher 或 server。VGE setup 同时负责安装依赖并初始化当前 worktree 的 `.env`，确保当前 worktree 中的 pdf-craft 及其 `doc-page-extractor` 依赖可以被导入和测试。\n\n`.env` 是 worktree 私有运行配置，不提交到 Git。VGE setup 在当前 worktree 缺少 `.env` 时，应优先从源工作区的 `.env` 复制，以便继承供应商 OCR 密钥和本机开发配置；只有源工作区也没有 `.env` 时，才从 `.env.template` 创建空配置。\n\n默认不配置 `scripts.archive`。VGE 会负责释放 worktree；项目级 Cleanup 只适合需要保留现场后执行额外归档或收尾动作的项目，本仓库当前没有这种需求。\n\n`pdf_craft_tool` 生成的转换输出默认留在当前 worktree 的 Git 忽略目录 `pdf-craft-output/` 下。模型缓存可能很大；只有在有意进行本地 OCR 工作时才使用 `models-cache/`，不要提交它，也不要假设其中已有内容。\n\n## 人类阅读文档\n\n面向人类贡献者的开发说明位于 `docs/DEVELOPMENT.md` 和 `docs/DEVELOPMENT_zh-CN.md`。README 文件应聚焦库使用者。Agent 面向的路由和项目约束保留在 `AGENTS.md` 和 `references/` 中。\n\n## 发布\n\n发版准备流程位于 [release-workflow.md](release-workflow.md)，各版本发布说明位于 `docs/changelog/`。除非任务明确要求发布或升级依赖，否则普通 worktree 支持改造不应修改包版本、依赖 pin 或发布元数据。\n","isInternal":false,"tokens":1185,"sizeBytes":3683},{"name":"release-workflow.md","path":"references/release-workflow.md","rawUrl":"https://raw.githubusercontent.com/oomol-lab/pdf-craft/HEAD/references/release-workflow.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# 发版流程\n\n**约束范围：** 发版准备、版本号对齐、变更日志要求，以及手动 GitHub Actions 发版工作流所需的仓库状态。**不约束：** 转换内部逻辑、与发版无关的依赖升级，以及手动发布 PyPI。\n\n## 发版模型\n\n发版由手动触发的 GitHub Actions 发版工作流执行。在运行该工作流之前，仓库必须先完成准备。该工作流是发版关口：它会从 `pyproject.toml` 读取版本号，检查仓库状态是否可发布，发布包到 PyPI，推送版本标签，并使用变更日志文件作为发布说明创建 GitHub Release。\n\nAgent 的工作应聚焦于准备仓库内的发版前置条件。发版准备过程中，不要手动发布 PyPI，不要手动创建 release tag，也不要手动创建 GitHub Release。\n\n## 准备发版\n\n对于目标版本 `X.Y.Z`，准备仓库时要确保以下事实成立：\n\n- [pyproject.toml](../pyproject.toml) 中的 `[project].version` 为 `\"X.Y.Z\"`。\n- `docs/changelog/vX.Y.Z.md` 存在，并包含发布说明。\n- 变更日志应包含简洁摘要；在有帮助时按类别组织变更；在可取得时链接相关 PR 或 issue；并包含完整变更日志的 compare 链接。\n- 发版准备变更应先通过普通 PR 流程审查，再从 `main` 运行手动发版工作流。\n\n发版准备 PR 应保持聚焦。不要把发版准备和无关重构或依赖变更混在一起，除非该版本本身就是围绕这些变更发布。\n\n## 验证\n\n发版准备使用常规轻量验证面：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\npoetry build\n```\n\n如果某个验证步骤无法在本地运行，应在交接说明中明确写出。\n\n## 变更日志说明\n\n发版工作流会直接读取 `docs/changelog/vX.Y.Z.md` 作为 GitHub Release 正文。变更日志文件应写成面向读者的发布说明，而不是内部实现说明。\n\n使用 `docs/changelog/` 中已有变更日志文件作为风格参考。发布标题和版本标签使用 `vX.Y.Z` 形式。\n","isInternal":false,"tokens":680,"sizeBytes":2065}],"systemPromptSnippet":"<yakaai_skills repo=\"oomol-lab/pdf-craft\">\n<!-- File: AGENTS.md (Tokens: ~873 | Category: root-instruction) -->\n# Agent 工作流\n\npdf-craft 是一个把扫描书籍 PDF 转换为 Markdown 或 EPUB 的 Python 库。本仓库使用 `~/.agents/skills/vibecoding` 作为通用维护工作法。本文件只记录 pdf-craft 特有的边界和按需阅读路由。\n\n## 工作区边界\n\n- `pdf_craft/` 是包源码。公共导入从 `pdf_craft/__init__.py` 暴露，便利入口在 `pdf_craft/functions.py`。\n- `tests/` 包含轻量单元测试和小型 PDF fixture。普通代码改动默认以这些测试作为验证面。\n- `docs/` 和 `README.md` 是读者/贡献者阅读的文档。不要把这些说明复制到 Agent 文档里。\n- `references/` 是 Agent 面向的引用文档。只阅读当前任务需要的引用文档。\n- `pdf_craft_tool/` 是未发布的本地 CLI，承载手动转换、翻译和冒烟矩阵；`scripts/` 只保留依赖源码同步辅助脚本。不要把它们当作默认开发流程。\n- `analysing/`、`pdf-craft-output/`、`models-cache/`、`.venv/`、`dist/`、`build/` 和 `*.egg-info` 是生成产物或本地运行产物。\n\n## 仅在需要时阅读\n\n- 当需要判断模块归属、公共 API 边界或新代码应放在哪里时，阅读[架构与模块边界](references/architecture.md)。\n- 当修改 PDF 提取、OCR 归一化、缓存 XML 产物、目录生成、章节生成、Markdown 渲染或 EPUB 渲染时，阅读[转换流水线](references/conversion-pipeline.md)。\n- 当修改 PDF 回填、QTextLayout、字号拟合、行数/单行规则或 PDF 文本层排版时，阅读[两阶段 PDF 文本排版](references/pdf-text-layout.md)。\n- 当选择 setup、验证、worktree 行为、发布或外部依赖处理方式时，阅读[开发与 Worktree](references/development-and-worktrees.md)。\n- 当准备发版、更新版本号、编写 changelog 或调整发布流程时，阅读[发版流程](references/release-workflow.md)。\n\n- 当修改失败恢复、翻译缺失、错误定位或任务成功判断时，阅读[失败与有效成果](references/failure-and-completion.md)。\n\n## 项目特有默认规则\n\n- 这是库项目。除非未来任务引入长期服务，否则不要启动常驻开发服务器。\n- 普通验证应避免 CUDA、模型下载、网络请求和完整 PDF 转换，除非任务明确触及这些行为。\n- 模型缓存和转换输出不得进入提交内容。`pdf_craft_tool` 的产物默认写入每个 worktree 自己的 `pdf-craft-output/`；只有任务明确需要 OCR 时，才考虑共享外部模型缓存。\n- 纯文档任务不得修改包代码或依赖版本。\n\n<!-- File: references/architecture.md (Tokens: ~2515 | Category: project-spec) -->\n# 架构与模块边界\n\n**约束范围：** 包结构、公共 API 和模块归属。**不约束：** OCR 算法细节、开发命令或发布步骤。**何时阅读：** 判断代码应放在哪里，或修改公共导入面时。\n\n## 包公共面\n\n## 新模块边界\n\n`pdf_craft` 的主要业务模块围绕可组合的文档处理阶段组织：\n\n- `extractor/`：PDF 页面、OCR、目录和章节分析的适配入口；产出 PDFCraftExtraction。\n- `document/`：PDFCraftExtraction 的 workspace/`.pcex` 存储、校验和来源位置契约。\n- `renderer/`：PDFCraftExtraction 到 Markdown 或 EPUB 的格式渲染入口。\n- `transformer/`：格式无关的 XML 内容变换，包括 LLM 翻译、结构填充和校验。\n- `pipeline/`：格式专属编排。EPUB Pipeline 将 EPUB XHTML/目录/元数据交给 Transformer；PDF Pipeline 以 replace-only 方式将 Chapter 来源区域写回 PDF。\n\n公开中间格式为 `PDFCraftExtraction`，持久化与交换载体必须是 `.pcex` ZIP。其内容为\n`manifest.json`、`pages.xml`、`chapters/`、`assets/`，以及可选 `toc.xml`、`cover.png`、\n`furnitures.xml` 和 v4 `translations/`。`translations/index.json` 按文件内唯一 ID 排列零到多个\n译文层；每层包含完整章节译文、metadata overlay、`coverage.xml`，以及可选 furniture 译文。\n目标语言只是层的元数据，同一种语言可以有多个 ID。根级 `translation.xml` 仍是旧式或内部物化\n译文视图的覆盖记录，不代表 v4 中可供渲染选择的持久化译文层。\n\nPCEX 格式版本以已经发布的 Git tag 为兼容基线，不随开发期间的中间 commit 逐次递增。只有准备\n新版本且格式相对上一个已发布版本发生 schema 变化时，才评估是否增加 `format_version`。当前 v4\n相对 `v2.3.1` 的 v3 增加了可叠加 translation layer；读取器继续接受 v1/v2/v3。\n\nPCEX chapter 的阅读流使用 `FlowItem`：`TextFlowItem` 是作者意义的正文/标题段，包含保留\n来源 bbox 的 `SourceTextFragment`，并可在 fragment 之间嵌入图片/表格 `SourceAsset`。\n`DisplayFormula` 与 `StandaloneAsset` 是独立 flow node；段落公式不是 AnchoredContent，必须\n阻断段落拼接。类名与 chapter XML 元素一一对应，详见 `docs/*/PCEX_FORMAT.md`。\nNarrative 翻译仅把嵌在 `TextFlowItem` 中的图片/表格替换为无文本的临时 anchor，不能读取或覆写\nasset 文本；`StandaloneAsset` 不伪造 paragraph anchor，并与 NarrativeFlow 隔离；\n`AnchoredContentTransformer` 是另一条按 asset 小批次工作的翻译边界，负责 title/content/caption。\n目录-backed 形态只供一键转换在 `analysing_path/extraction/` 内部衔接前后端；普通目录不是\n公开输入。`ocr/`、`plots/`、`done` 和其他 analysis 文件仅是可丢弃的诊断/恢复缓存。\n\n`pdf_craft/__init__.py` 是公共导入面。`AsyncPDFCraft` 是业务核心，`PDFCraft` 是位于\n`pdf_craft/sync/` 的同步门面；`pdf_craft/transform.py` 是 PDF 前端\n提取 engine。Markdown、EPUB、翻译和 PDF 写回后端只能读取 PDFCraftExtraction，不得读取\nanalysis/OCR 缓存。\n\n`PDFCraftExtraction` 是公开但不包含业务方法的 opaque handle。用户只能通过两个 Craft 门面的\n`open_extraction` / `export_extraction` 打开和导出，再把句柄传回门面或高级组件。高级可组合组件\n（Extractor、Renderer、Transformer、LLM runtime）仅提供无 `_async` 后缀的异步方法；同步适配\n不得进入业务核心。配置、选项、事件和数据类不拆分同步/异步版本。模型下载同样以 async 实现为准，\n同步使用通过 `PDFCraft.predownload_models()` 委派。\n\n业务执行以 async 为唯一真源：请求、重试、repair loop、编排和转换不能再提供第二套同步实现。\n`run_sync` 只能由 `pdf_craft/sync/` 的用户门面调用；禁止在实现层启动事件循环把同步业务伪装成\n异步业务。用户提供的 LLM、Transformer、TOC 转换、PDF handler 和图片 URL resolver 执行方法\n必须是异步契约，不提供同步扩展适配。普通事件通知可以接受同步或异步 callback。\n仅当第三方库没有异步接口时，才在独立执行域中调用同步适配器；适配器只承担第三方调用、\n文件读写和必要资源清理，不复制业务编排。跨域 callback 桥接不得成为另一条请求或转换路径。\n\n`translate_extraction()` 保留 source layer 并追加 replacement-only 译文层。Markdown/EPUB 渲染\n通过 `RenderMode.SOURCE`、`REPLACE`、`BILINGUAL` 决定只读原文、选择一份译文或合并原译文；\n`BILINGUAL_TEXT` 在同一来源 fragment 内追加译文。这些译文模式可显式传 translation ID，省略时稳定选择 index 第一项。PDF 写回也可显式选择译文层，\n但仅消费已有正文和 furniture 写回能力，不写回 anchored 图片/表格文字。\n\n除非任务明确要求破坏性 API 变更，否则把以下名称和默认值视为公共 API：\n\n- `PDFCraft`、`AsyncPDFCraft`、`PDFOptions`、`ExtractionOptions`、`PDFCraftExtraction`\n- `PDFExtractor`、`MarkdownRenderer`、`EpubRenderer`\n- `ExtractionTransformer`、`NarrativeXMLTransformer`\n- `AnchoredContentExtractionTransformer`、`AnchoredContentTransformer`、`AnchoredContentXMLTransformer`\n- `predownload_models`\n- `LLM`\n- `JEV`、`FootnoteOptions`、`FootnoteRefinement`\n- `DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`\n- `DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`\n- `AsyncPDFHandler`、`AsyncPDFDocument`\n- `BookMeta`、`TableRender`、`LaTeXRender`\n\n## 模块归属\n\n- `pdf_craft/pdf/` 负责 PDF 元数据、渲染、页引用、通过 `doc-page-extractor` 接入 OCR 后端，以及 OCR 页 XML 数据。\n- `pdf_craft/extractor/toc/` 负责目录页检测和标题层级分析，包括可选的 LLM 辅助分析。\n- `pdf_craft/extractor/chapter/` 负责根据 OCR 页 XML 和 TOC 事实生成章节结构。\n- `pdf_craft/markdown/` 负责 Markdown 段落解析和 Markdown 输出渲染。\n- `pdf_craft/renderer/epub/` 负责把章节数据转换为 `epub-generator` 的记录并生成 EPUB。\n- `pdf_craft/llm/` 负责增强目录分析所需的可选 LLM 调用。核心转换应保持不依赖该增强能力也可使用。\n- `pdf_craft/common/` 负责可复用的文件系统、XML、资源和统计辅助逻辑。\n\n## 外部包边界\n\n`doc-page-extractor` 和 `epub-generator` 是被 pin 住的运行时依赖。它们内部的问题通常应在各自仓库修复，再通过版本升级或明确的本地联调引入。本仓库 `scripts/` 下的同步脚本会覆盖 `.venv` 中已安装的依赖源码；这些脚本只是手动本地联调辅助，不是普通项目 setup。\n\npdf-craft 对外只暴露自己的 OCR 配置对象，不暴露 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 注入口。需要新增 OCR 后端时，优先在 `doc-page-extractor` 增加官方构造入口，再在 pdf-craft 映射成封闭配置对象。\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不要把 `torch` 或 `torchvision` 作为 pdf-craft 的直接运行时依赖；用户仍可能需要按自己的环境覆盖安装 CPU 或 CUDA wheel。\n\n<!-- File: references/conversion-pipeline.md (Tokens: ~4986 | Category: anthropic-skill) -->\n# 转换流水线\n\n**约束范围：** PDF 到输出文件的流程、中间产物和转换契约。**不约束：** 通用 setup 或打包。**何时阅读：** 修改提取、TOC、章节生成、Markdown 渲染或 EPUB 渲染时。\n\n## 运行时流程\n\n## 组合流程\n\nExtractor 生成 PDFCraftExtraction 后，Renderer 可直接生成 Markdown 或 EPUB。可选 Transformer\n可以在渲染前修改结构化文本；`pipeline/epub` 也可把既有 EPUB 的 XHTML、目录和元数据交给\n同一个 XML Transformer。PDF Translation Pipeline 只处理已记录来源 bbox 内的文本。\n\nPCEX 翻译不再覆盖 source：`translate_extraction()` 在 v4 `translations/` 下追加一个完整的\nreplacement-only 层。Markdown/EPUB 随后选择 source、某一译文替换或 source 加某一译文；选择\n依赖 translation ID，未指定时使用 index 第一项。这个渲染选择与直接 EPUB 输入的 append/replace\n提交模式是不同阶段的概念。一键 PDF→Markdown/EPUB 也复用 replacement-only 译文层，显式\n选择本次新生成的 ID；REPLACE / APPEND_TEXT / APPEND_BLOCK 分别映射为 REPLACE /\nBILINGUAL_TEXT / BILINGUAL 渲染。只有临时展示视图组合原译文，不把重复来源写入待校验\nPCEX。BILINGUAL_TEXT 在同一来源 fragment 内追加内容；BILINGUAL 按正文段追加译文。\n资产先按稳定 flow/child 索引组合，再插入双语段落；表格在单元格内组合，不能追加第二张表。PDF Translation Pipeline 也可通过显式 translation ID 选择持久化译文；\n省略 ID 时继续消费根内容。PDF 写回只处理 narrative 与 furniture，不处理缺少字段级来源坐标的\nanchored 图片/表格文字。\n\n`AsyncPDFCraft.convert_pdf_to_markdown()` 和 `AsyncPDFCraft.convert_pdf_to_epub()` 会先提取到内部 workspace，\n再渲染目标输出。提取流程是：\n\n1. 通过 `PDFHandler` 渲染 PDF 页面。\n2. 通过 `OCR.recognize()` 识别页面布局。\n3. 在 `analysing_path/ocr/` 写入 OCR 页 XML 和恢复缓存，在\n   `analysing_path/extraction/assets/` 写入资源。\n4. OCR 的终态页事件经磁盘流恢复输入页序，传统跨页解析与 OCR 并行推进；闭合正文、脚注和\n   未定归属来源存入磁盘数据库。可选 `on_body` / `on_footnote` 交付独立 PCEX XML 快照，\n   回调背压由磁盘队列与 OCR 生产隔离。EOF 排空后再分析 TOC 数据。\n5. 在 `analysing_path/extraction/` 生成章节、TOC、页面几何和 manifest；章节使用 PCEX v3\n   FlowItem，保留文字 fragment、段内 anchored image/table 与独立段落公式的阅读顺序。\n6. 仅从该 extraction 渲染 Markdown 或 EPUB。\n\n章节生成在第 5 步内部先为 TOC 排除后的 OCR Page 建立完整来源记录：保留原文、原分类、\nOCR 流内下标和独立阅读位置。正文沿用原 source_order；新提取的脚注使用正文布局数作为\n偏移，写入同一页的 fragment 身份空间。OCR Page 的 order 不变，旧 PCEX 不重新编号。\n阅读顺序保留正文已有的栏序，将脚注分类中的来源按同栏位置插入，不按全页 y 坐标排序。\n通宽锚点限定阅读带；栏尾来源插在所属栏最后成员之后，不能退回整页或整带末尾。\n传统 join、citation 拆分和同页 ref 匹配只建立初始结论，再投影为 PageAnalysis。\n跨页合并必须把页尾/续页页首 image/table 保留在两侧文字之间，不可挪到整段之后。\nPageAnalysis 的 continuation 表示文字前驱，跳过同一归属流中的图片/表格；资产自身 gap\n保持 false，DisplayFormula 才是硬边界。投影与恢复必须保留段内锚点，精修与校验遵守同一规则。印刷标记的\n完整来源和前缀范围保留到组装：正文投影保留前缀，定义投影才移除前缀。断词修复延后到 ownership\n组装完成后的语义投影；原始文字记录不被改写，投影为空的 fragment 也保留原身份和 bbox。\n\n默认传统算法由 `extractor/chapter/stream.py` 增量连接、来源投影和磁盘组装；无目录页排除时复用\n在线生成的内容数据库，有目录页排除时从 OCR 缓存按排除规则重放。章节定稿逐章处理。\n需要可编辑页级 ownership 的 PageAnalysisProcessor 仍使用 `restore_streams` 的整批事务语义；\n在线回调发生在可选 refinement 之前，不声明是修正后归档的重放。\nFootnoteOptions(refinement=FootnoteRefinement(...)) 在调用方 event loop 在线执行 JEV 筛选及\nLLM 页级修正；CLI 的 --footnotes 只启用传统算法。LLM JSON 通过 request_guaranteed_json\n编译 ownership、必需元数据、引用位置及定义关系，候选在完整暂存对象图上组装成功才接纳。\n候选在暂存对象图通过校验后才提交；允许降级的页保留算法结果，必要时撤销整批精修，任何失败均不修改调用方原对象。不得额外嵌套请求重试。\nref 仍以 layout_id + citation_index + before/mark/after 定位，不要求 LLM 算 offset。\n\n连接的唯一编辑真源为后继的 continues_from_previous；continues_to_next 由此派生，旧字段若\n显式返回，仅作为同页一致性断言。没有按修正先后顺序选择布尔值的 reconciliation。\n已知跨页定义 ID、起点索引与既有跨页片段不可由单页请求删除或重命名。保护同时覆盖 segment\n记录与既有 layout 成员，不能保留 ID 却将成员改为 ignored、正文或其他定义；内部自然段边界仍可编辑。页内非共享定义可以\n通过重分组拆分或合并，新增 ID 为 new-页码-index；未确定归属的续段可明确连接到上一页已知\n定义。无索引段排在本页有索引段前，有索引定义从 1 连续递增。没有起点的已知 continuation\n是非法对象图；unresolved 来源则显式保留为可读正文，不能猜测归属后丢弃。\n\n一条定义可以有零个或多个引用出现；数字、圈号及星号都属于印刷标记域，标记不是定义身份。\n未匹配定义由 Chapter.definitions 保留，经既有 PCEX references 容器编码，无需制造正文 ref。\nMarkdown/EPUB 保留这些定义。正文及脚注 TextFlowItem 共用 narrative_texts 遍历，翻译 coverage\n和 PDF 计划消费同一来源映射；有 coverage 的脚注正文实际回填，无 coverage 的区域仍作障碍。\n多次引用或跨章节复用同一来源不重复回填。定义开头的印刷标记在 PDF 输出时恢复。\n格式专有适配留在 PCEX 层，通用 XML translator 无需了解脚注。\n\nPCEX 格式保持兼容。现有归档和译文层按原身份读取、校验和导出，不做推测重编号；\n缺失 coverage 继续表示 preserved。旧归档若同一章节已有歧义的重复 fragment 身份，仍明确拒绝，\n不声称能够无损迁移。更详细的修复前后证据见 docs/verification/footnote-788.md。\n\n当未传入 `analysing_path` 时，`EnsureFolder` 会创建临时目录。当传入该路径时，它会成为可持久复用的缓存和调试输出目录。\n\n## 中间产物契约\n\nanalysis 与稳定 extraction 明确分离：\n\n- `ocr/page_*.xml`：OCR 页数据。\n- `ocr/done`：表示所有选中页面已完成识别的标记。\n- `plots/`：启用 plot 生成时的可选可视化调试输出。\n- `extraction/manifest.json`：格式版本、producer、创建时间和文档元数据。\n- `extraction/pages.xml`：1-based OCR 像素坐标空间、实际渲染 DPI 和逐页像素宽高。\n- `extraction/assets/`：按内容 hash 存放裁剪出的图片、公式和表格。\n- `extraction/chapters/chapter_*.xml`：生成的 v3 FlowItem 章节记录及原 PDF page/bbox 映射；\n  图表可嵌在 TextFlowItem fragment 之间，DisplayFormula 必须独立且不能被段落拼接跨越。\n- `extraction/toc.xml`、`extraction/cover.png`：可选目录和封面。\n- `extraction/furnitures.xml`：可选的页面家具 pattern 与页级 section。\n- `extraction/translations/index.json`：v4 可选译文索引；条目以 ID 区分，记录目标语言和创建时间。\n- `extraction/translations/<id>/chapters/`、`metadata.json`、`coverage.xml`：一份独立译文层；章节\n  结构与 source 对齐，coverage 记录 Narrative、furniture position/section 及独立图片/表格文本的\n  `translated` / `preserved` 状态。可选 `furnitures.xml` 保存该层的 furniture 译文。\n- `extraction/translation.xml`：旧式或内部临时物化译文视图的根级 coverage；不要把它当作 v4\n  translation layer 索引。\n\nNarrative 翻译中的段内 asset 只以无文本临时 anchor 保持位置；独立 asset 不伪造 anchor。图片/\n表格 title/content/caption 属于独立翻译阶段；标准高层翻译入口固定先运行 NarrativeFlow，随后由\n`with_anchored` 决定是否运行该阶段，并以 anchored coverage 约束 v4 translation layer 中允许变化的 slot。\n\n公共分段流程把 `extraction/` 打包为 `.pcex`；恢复后端只接受 `.pcex` 或已加载的\n`PDFCraftExtraction`。一键转换直接使用 workspace，只有显式 `extraction_path` 时才额外导出\n`.pcex`，避免压缩往返。翻译后的 `.pcex` 必须保留 source 的 manifest、pages、章节、TOC、封面、\nfurniture 和资源，并把每份译文及其 coverage 隔离在对应 translation layer 中。\n`PDFCraftExtraction` 只作为 opaque handle 流转；归档的打开、校验和导出由 `AsyncPDFCraft`\n负责，`PDFCraft` 仅同步转发这些操作。\n\n修改 XML schema、文件命名或跳过语义会影响多个模块，应视为跨流水线变更，并配套有针对性的测试。\n\nMarkdown/EPUB 只从 FlowItem 的阅读顺序渲染，不能也不应复刻固定 PDF 页面几何：Markdown 将\n段内图片/表格安全拆成块级内容；EPUB 默认同样处理，仅在小型图片的相邻 fragment 提供同页、\n另一侧、纵向重叠等强几何证据时尝试可降级的左右 float。表格和 DisplayFormula 不得 float；\n阅读器不支持 CSS 或窄屏时仍必须保留正常块级顺序。\n\n## 重型运行时边界\n\n`PageExtractorNode` 会延迟导入 `doc-page-extractor`，并且只在需要 OCR 时根据 pdf-craft 的 OCR 配置创建上游 extractor。除非任务明确要求 eager loading，否则应保持这种延迟加载行为。\n\nOCR 配置是封闭公共面：`DeepSeekOCRLocalConfig`、`DeepSeekOCR2LocalConfig`、`UnlimitedOCRLocalConfig`、`DeepSeekOCRVendorConfig`、`DeepSeekOCR2VendorConfig`、`UnlimitedOCRVendorConfig`。不要把 `doc-page-extractor` 的 `PageExtractor`、`OCRAdapter` 或 factory 作为 pdf-craft 公共注入口。\n\n本地 OCR 可能需要 Poppler、支持 CUDA 的 PyTorch、大型模型下载和较高显存；供应商 OCR 不需要本地 CUDA，但需要网络和密钥。普通单元测试应保持不依赖这些资源也能运行。\n\n## 错误与恢复语义\n\n失败决策、可信译文缺失与任务级成功的原则见 [失败与有效成果](failure-and-completion.md)。页面级降级会保留图片或明确占位，全部主要工作降级仍须报错。\n\n已存在的 `page_*.xml` 会被跳过。`done` 标记会让 OCR 识别整体跳过。修改恢复行为时要谨慎，因为它同时影响本地手动运行和 VGE worktree 重跑。\n\n## EPUB 原子翻译回调\n\nEPUB 适配器异步逐项读取任务，在共享 XMLTranslator 的局部映射采纳位置接收结果，按章写入\n临时归档；不收集全书正文任务或完整译文结果。同步 iterable 的既有核心接口继续供 PCEX 翻译使用。\n可选 `on_body` 接收 `EpubFragmentContent(epubcfi, original, translated)`，只包含去掉窗口上下文后的\n原子片段；来源 CFI 索引由 EPUB 适配器按需构建，源范围贯穿公式替换。`pipeline/epub/cfi.py`\n只负责文档内定位，OPF/spine 前缀由外层提供。用户说明见 `docs/*/STREAMING.md`。\n\n\n## PCEX 内容流与页面查询\n\n`document/content.py` 逐 XML 内容项解码现有 FlowItem/Reference，body 中引用只携带身份，完整\n定义走独立条目。`document/stream_view.py` 提供固定资源/目录/页面几何与原文/所选层/在线译文流；\n标准 XMLTranslator 不先生成全书译文 workspace。`transformer/content.py` 复用同一引擎，保留\n资源前后文，PCEX writer 是该流的一个消费者。自定义整章 translator 保留显式兼容路径。\n\nMarkdown 逐项写暂存内容，EPUB 逐项转换并 spooling EPUB records，PDF 以来源顺序的磁盘\nfeed 向隔离 Qt 进程提交替换项。PDF 真实布局通过磁盘回传，跨页段落在所有区域排版定稿后\n一次通知；窗口只按现有页级约束收尾，不拿内容回调冒充排版通知。\n\n公共 `document/paragraph.py` 定义 SourceRegion、ParagraphContent、PDFSourceRegion 与\nPDFParagraphContent。翻译/重排渲染使用独立 ParagraphContent（Markdown text、可选 original、\n来源 regions），PDF 使用每区域带字号/行/公式/擦除的 PDFParagraphContent。提取仍保留原有\nXML 回调；EPUB 输入翻译仍保留 EpubFragmentContent。未提供通道时不封装该通道的公共产物。\nSourceRegion.page 复用 PageGeometry，source_order_in_page 对应 PCEX source_order；bbox\n仍为 OCR 像素整数，map_bbox 根据实际页面尺寸返回 float。render_dpi 是请求值，不能单独\n作为缩放依据。PDF 公共布局的坐标与字号也换算为来源 OCR 像素，不引入 font_id。\n\n`document/access.py` 的 read_pages/read_asset 与 ExtractionContext 继续作为公共几何/资源\n访问入口。内容对象包含页面几何，因此回调消费者不必再做页面联表。资源只通过受支持的\n字节读取入口获取，不依赖临时目录。\n\nLLM 有序交付使用磁盘重排缓存；异步任务上下文、来源 DOM 与完成结果都可落盘，通过\n稳定 root/node identity 在当前文档交付时恢复映射，队首慢请求不再保留全书原文 DOM。\n同步 iterable/自定义整章契约保留原对象身份。双阶段 vendor OCR 续阶段优先，未确定归属来源\n按阅读顺序发出，延续 #794/#795 回归。\n\n\n元信息通知复用 `document/metadata.py` 的 MetadataContent/DocumentMetadata。\nPDF 原生-only 通知在 OCR 前，LLM 提取通知在字段合并后；异常不能被提取降级逻辑吞掉。\nPCEX 的共享 NarrativeXMLTransformer 元信息阶段负责翻译通知，EPUB 适配器负责 OPF 视图通知；\n书籍通知不包含 TOC 或文档 head/title。只有用户提供 on_metadata 时才构造独立公共快照。\nEpubTranslation 仅作为 EpubFragmentContent 的兼容别名。\n\n<!-- File: references/development-and-worktrees.md (Tokens: ~1185 | Category: anthropic-skill) -->\n# 开发与 Worktree\n\n**约束范围：** setup、验证、VGE worktree 行为和发布命令。**不约束：** 包架构或转换内部逻辑。**何时阅读：** 选择命令、编辑项目配置或修改开发文档时。\n\n## 环境\n\n使用 Python `>=3.11,<3.14`；默认开发版本是 Python 3.11。本仓库使用 Poetry 2.x；配置 `poetry config virtualenvs.in-project true` 时，虚拟环境应位于当前 worktree 内。\n\n普通开发安装依赖：\n\n```bash\npoetry config virtualenvs.in-project true\npoetry install --with dev\n```\n\n本包通过 `doc-page-extractor[local]` 获得上游本地 OCR 运行时栈，但不直接声明 `torch` 或 `torchvision`。只有当任务需要指定 CPU 或 CUDA wheel 时，才按当前环境覆盖安装 `torch` 和 `torchvision`。\n\n真实 OCR 有六种配置入口：\n\n- `DeepSeekOCRLocalConfig`：本地 DeepSeek OCR，真实转换需要 CUDA。\n- `DeepSeekOCR2LocalConfig`：本地 DeepSeek OCR 2，真实转换需要 CUDA。\n- `UnlimitedOCRLocalConfig`：本地 Unlimited OCR，真实转换需要 CUDA。\n- `DeepSeekOCRVendorConfig`：DeepSeek OCR 供应商模式。\n- `DeepSeekOCR2VendorConfig`：DeepSeek OCR 2 供应商模式。\n- `UnlimitedOCRVendorConfig`：Unlimited OCR 供应商模式。\n\n库代码不得自动读取 `.env`。未发布的 `pdf_craft_tool` CLI 通过 `pdf_craft_tool/runtime.py` 加载工作区 `.env` 并创建显式配置对象。所有手动运行时变量使用 `PDF_CRAFT_` 前缀：OCR 使用 `PDF_CRAFT_OCR_MODE` 和对应的 `PDF_CRAFT_*_OCR_*` 配置；文本 LLM 通过可复用的 `PDF_CRAFT_LLM_<PROFILE>_*` profile 配置。默认 profile 可在运行时从 `oo llm config --json` 获取 OOMOL 凭据，不能复用 OCR-only endpoint。\n\n## 默认验证\n\n优先选择能跨越改动边界的最小命令集：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\n```\n\n`poetry build` 用于验证打包。`scripts/` 中的完整转换脚本是手动检查，可能需要 Poppler、PyTorch、模型缓存和 CUDA。\n\n## VGE Worktree 行为\n\n本项目没有长期运行的开发服务，因此 `.conductor/settings.toml` 不应定义 `scripts.run`，除非未来项目新增 watcher 或 server。VGE setup 同时负责安装依赖并初始化当前 worktree 的 `.env`，确保当前 worktree 中的 pdf-craft 及其 `doc-page-extractor` 依赖可以被导入和测试。\n\n`.env` 是 worktree 私有运行配置，不提交到 Git。VGE setup 在当前 worktree 缺少 `.env` 时，应优先从源工作区的 `.env` 复制，以便继承供应商 OCR 密钥和本机开发配置；只有源工作区也没有 `.env` 时，才从 `.env.template` 创建空配置。\n\n默认不配置 `scripts.archive`。VGE 会负责释放 worktree；项目级 Cleanup 只适合需要保留现场后执行额外归档或收尾动作的项目，本仓库当前没有这种需求。\n\n`pdf_craft_tool` 生成的转换输出默认留在当前 worktree 的 Git 忽略目录 `pdf-craft-output/` 下。模型缓存可能很大；只有在有意进行本地 OCR 工作时才使用 `models-cache/`，不要提交它，也不要假设其中已有内容。\n\n## 人类阅读文档\n\n面向人类贡献者的开发说明位于 `docs/DEVELOPMENT.md` 和 `docs/DEVELOPMENT_zh-CN.md`。README 文件应聚焦库使用者。Agent 面向的路由和项目约束保留在 `AGENTS.md` 和 `references/` 中。\n\n## 发布\n\n发版准备流程位于 [release-workflow.md](release-workflow.md)，各版本发布说明位于 `docs/changelog/`。除非任务明确要求发布或升级依赖，否则普通 worktree 支持改造不应修改包版本、依赖 pin 或发布元数据。\n\n<!-- File: references/release-workflow.md (Tokens: ~680 | Category: anthropic-skill) -->\n# 发版流程\n\n**约束范围：** 发版准备、版本号对齐、变更日志要求，以及手动 GitHub Actions 发版工作流所需的仓库状态。**不约束：** 转换内部逻辑、与发版无关的依赖升级，以及手动发布 PyPI。\n\n## 发版模型\n\n发版由手动触发的 GitHub Actions 发版工作流执行。在运行该工作流之前，仓库必须先完成准备。该工作流是发版关口：它会从 `pyproject.toml` 读取版本号，检查仓库状态是否可发布，发布包到 PyPI，推送版本标签，并使用变更日志文件作为发布说明创建 GitHub Release。\n\nAgent 的工作应聚焦于准备仓库内的发版前置条件。发版准备过程中，不要手动发布 PyPI，不要手动创建 release tag，也不要手动创建 GitHub Release。\n\n## 准备发版\n\n对于目标版本 `X.Y.Z`，准备仓库时要确保以下事实成立：\n\n- [pyproject.toml](../pyproject.toml) 中的 `[project].version` 为 `\"X.Y.Z\"`。\n- `docs/changelog/vX.Y.Z.md` 存在，并包含发布说明。\n- 变更日志应包含简洁摘要；在有帮助时按类别组织变更；在可取得时链接相关 PR 或 issue；并包含完整变更日志的 compare 链接。\n- 发版准备变更应先通过普通 PR 流程审查，再从 `main` 运行手动发版工作流。\n\n发版准备 PR 应保持聚焦。不要把发版准备和无关重构或依赖变更混在一起，除非该版本本身就是围绕这些变更发布。\n\n## 验证\n\n发版准备使用常规轻量验证面：\n\n```bash\npoetry run pyright pdf_craft tests\npoetry run pylint pdf_craft tests\npoetry run python test.py\npoetry build\n```\n\n如果某个验证步骤无法在本地运行，应在交接说明中明确写出。\n\n## 变更日志说明\n\n发版工作流会直接读取 `docs/changelog/vX.Y.Z.md` 作为 GitHub Release 正文。变更日志文件应写成面向读者的发布说明，而不是内部实现说明。\n\n使用 `docs/changelog/` 中已有变更日志文件作为风格参考。发布标题和版本标签使用 `vX.Y.Z` 形式。\n</yakaai_skills>"}