# Scanned PDF To Epub

> 将扫描版 PDF 书籍（内页全为图片、无文字层）转换为带完整导航目录、可点击跳转的可读 EPUB。 识别由 Claude 视觉子代理（Sonnet 模型）逐页读图完成，CLI 脚本负责 拆页渲染、标记检查、批次合并、按章节切分打包、EPUB 校验。当用户提到"扫描 PDF 转电子书 / PDF 转 EPUB / 扫描书转 epub / 把这本书做成 epub / 扫描件转可读格式 / 图片 PDF 生成目录"时，务必使用本 skill， 即使对方没有明确说出"skill"或"epub"字样。

- Skill: `wmy2981/scanned-pdf-to-epub` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add wmy2981/scanned-pdf-to-epub`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wmy2981/scanned-pdf-to-epub/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: wmy2981 (https://skillmd.com/u/wmy2981)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/wmy2981/scanned-pdf-to-epub

---


# PDF → EPUB（扫描书转换工作流）

## 何时使用

- 输入是一本 PDF 书籍，**内页全是扫描图片、没有文字层**（用 PyMuPDF 提取不出文本，或全是空白）。
- 目标产物是一本**干净、带完整导航目录、目录可点击跳转**的 EPUB。
- 中间产物可以是 Markdown。
- 本 skill 面向**通用流程**：任何页数的纯文字中文书籍，不针对某一本书特化。

本 skill 不适用于：有文字层的数字原生 PDF（直接用提取工具即可）；漫画书（图为主，本流程去除插图）。

## 核心原则：职责边界

| 环节 | 承担方 | 理由 |
|---|---|---|
| 拆页渲染、标记检查、批次合并、分章、打包、校验 | **CLI 脚本**（`scripts/`） | 确定性、可复现、可重跑 |
| 识图、错别字校对、版权页判定、锚点转标题 | **Agent**（Claude 子代理 / 主代理） | 需要视觉与语义判断，**不写脚本** |
| 全流程编排与规范 | **SKILL.md** | 承载流程与严谨规则 |

**模型约定**：识图子代理**必须使用 Sonnet 模型**；不要使用外部 API。主代理校对由当前会话模型完成。

**不要为识图或校对环节编写脚本**——那部分是 Agent 的灵活任务，脚本只覆盖五个确定性命令：`extract`、`check-markers`、`merge-batches`、`md-to-epub`、`verify`。

## 工作流总览

```
1. extract (CLI)        PDF → 每页一张图 page_0001.png...
2. 子代理识图 (Agent)    每子代理一批 → batch_001.md（纯文本行 + 锚点）
3. 批次标记检查 (CLI)    check-markers 检查各 batch 标记 → 有误重跑、无误通过
4. 批次合并 (CLI)        merge-batches 按序合并 → draft.md
5. 草稿标记复查 (CLI)    check-markers 复查 draft.md 标记
6. 主代理校对 (Agent)    修正错别字（不改原文）→ 锚点转 h 标题 → book.md
7. md-to-epub (CLI)     book.md → 按 h 标题切分章节 → 生成导航目录 → book.epub
8. verify (CLI)          校验 EPUB 结构、目录跳转、无残留
```

## 依赖

- Python 3.10+，`pymupdf`（渲染 PDF 页为图，`import fitz` 或 `import pymupdf`）
- pandoc（Markdown → EPUB，含导航目录）。安装顺序：
  1. 系统 CLI pandoc（`winget install JohnMacFarlane.Pandoc` / `brew install pandoc`）
  2. 或 `pip install pypandoc-binary`（捆绑 pandoc 二进制，无需单独安装）
  3. 兜底：`pip install ebooklib`（纯 Python 打包，目录同样生成）
- 脚本自动探测上述任一可用路径。

启动前先检查：`python scripts/extract.py --check` 会报告缺哪些依赖。

## 工作目录布局

在临时目录内新建一个项目文件夹 `book/`，全部产物放其中：

```
book/
├── pages/            # 1. 拆页图片 page_0001.png...
├── raws/             # 2. 子代理识图输出 batch_001.md...
├── draft.md          # 3-5. 批次合并 + 标记检查后的草稿（仍含 PAGE/CH/__ 标记）
├── book.md           # 6. 校对优化后的全文（干净层级化 Markdown）
└── book.epub         # 7. 最终产物
```

## 1. extract：拆页渲染（CLI）

```bash
python scripts/extract.py <input.pdf> -o book/pages [--dpi 300]
```

- 每页渲染为一张 PNG，文件名按原书页码顺序 `page_0001.png`、`page_0002.png`……（从 1 起连续编号，忽略 PDF 内部无关页面偏移）。
- 默认 300 DPI，足以支撑中文小字识别；输出时打印总页数，供调度子代理参考。
- 结束后核对 `pages/` 图片数量 == PDF 页数。

## 2. 子代理识图（Sonnet 子代理）：逐页转纯文本行 + 锚点

### 分派规则

- **模型**：本阶段识图**一律使用 Sonnet 模型子代理**（Sonnet 具备本环境识图能力），严禁使用外部 API 或其他模型；主代理校对由当前会话模型完成。
- **每个子代理负责连续 10 张图片**：子代理 #1 负责 `page_0001`–`page_0010`，#2 负责 `page_0011`–`page_0020`……批次严格按页码顺序分派。
- 批次边界：第 k 个子代理负责 `page_{(k-1)*10+1:04d}` ～ `page_{k*10:04d}`；最后一批不足 10 页则只处理实际存在的页。
- **并发由主代理自行决定**：一次可同时调度多个子代理，各自负责不同批次，批次间不重叠、顺序不乱。并发数与批次划分由主代理按页数规模、成本与运行环境权衡，不设固定上限。
- **每子代理一个文件**：子代理把自己负责的整批页内容写入同一个文件 `batch_{k:03d}.md`（命名规范见 `references/subagent_prompt.md`），文件内**每页（含空白页、无正文页）**均以 `<!-- PAGE XXXX -->` 注释定位（空页只写标记不写内容）。失败重跑以批次为单位。

### 子代理提示词模板（引用 references/subagent_prompt.md）

调度每个子代理时，直接使用 **`references/subagent_prompt.md`** 中的提示词模板：把 `{N}`、`{起始页}`、`{结束页}`、`{批次序号}`、`{pages_dir}`、`{raws_dir}` 替换为实际值后作为子代理任务传入（识别必须用 Sonnet 模型）。该文件是提示词的唯一正式来源，本小节不再内嵌模板正文。
**调度子代理时，仅可替换模板中 `{占位符}` 标记的内容，模板其余部分必须原样保留，禁止增删或修改。**

### 子代理严格输出约束（要点）

子代理的唯一输出规范以 **`references/subagent_prompt.md`** 的提示词模板为准，要点归纳如下；完整条款见模板与下方「锚点规范」「内容保留 / 去除规则」：

- **输出纯文本行**：原文一个自然段 = Markdown 一行，段落间空行；**不使用**列表、表格、代码块等结构标记。
- **跨页段落**：一个自然段被分页截断时，上一页行尾与下一页行首各写 `__` 连接标记（两部分间不放空行），校对时合并为一段。
- **禁止**：任何解释、说明、思考过程、结论、"这是第 X 页"之类元评论；禁止页眉页脚文字；禁止对图片的描述性旁白。
- **每子代理一个文件** `batch_{k:03d}.md`：**严格按 `page_XXXX.png` 文件名的数字顺序**逐页读取，整批页内容顺序写入同一文件；每页（含空页）以 `<!-- PAGE XXXX -->` 定位（**XXXX 严格等于该图片文件名的页码**：读 `page_0005.png` 写 `<!-- PAGE 0005 -->`）。
- 遇到**章节标题**时，插入**层级前缀式锚点**（见下），正文段落里**不重复**标题文字；层级如实反映原书结构，不主观合并或拆分。
- **保留**：前言、后记（标题用锚点标注层级）、各章节章号/大标题、正文段落；原书目录页照常识别。
- **去除**：页眉、页脚、插图、二维码/条形码/水印等无关标识。
- 遇到疑似**版权页**（整页版权声明模式：ISBN / 版权年份 / 出版信息 / 印次号）时，整页内容以单个锚点 `<!-- COPYRIGHT_CANDIDATE -->` 起始，交由主代理终审。
- **完成后汇报**：顺利则只返回一句话说明已写入的文件名；遇阻断或不确定点（图片无法读取、内容严重残缺、规则无法判断）如实反馈主代理，不自行臆断、跳过或编造内容。

### 锚点规范（层级前缀式）

每遇到一个标题，插入一行注释，`CH` 后的数字表示层级：

```markdown
<!-- CH1: 第二部 江湖 -->

<!-- CH2: 第1章 初入师门 -->

<!-- CH3: 第一节 拜师 -->
```

- `CH1` = 全书最高级标题：有部/卷则部/卷为 CH1、章为 CH2、节为 CH3；**无部/卷的书，章直接为 CH1**（保证顶级章节转成 h1，pandoc 才能按章切分，否则全书挤成单一文件）。
- 标题文本**只出现在锚点中**，正文段落不重复标题。
- 锚点为独立注释行，前后以空行与正文隔开；锚点之间是正文（段落，一个自然段 = 一行）。
- 规则：遇到一个新标题就按其层级插入对应锚点；层级如实反映原书结构，不要主观合并或拆分。

### 内容保留 / 去除规则

| | 内容 | 处理 |
|---|---|---|
| ✅ 保留 | 前言、后记 | 作为正文保留（标题用锚点标注层级） |
| ✅ 保留 | 各章节章号/大标题 | 插入锚点，作为分章依据 |
| ✅ 保留 | 正文段落 | 每段一行，段落间空行 |
| ❌ 去除 | 页眉、页脚 | 直接丢弃，不写入 |
| ❌ 去除 | 插图（含示意图、照片） | 直接丢弃，不写入（本 skill 面向纯文字书籍） |
| ❌ 去除 | 版权页（整页版权声明） | 用 `<!-- COPYRIGHT_CANDIDATE -->` 标记，主代理终审 |
| ❌ 去除 | 无关标识（二维码、条形码、水印、页码） | 直接丢弃 |
| ⚠️ 特殊 | 原书目录页 | 子代理照常识别正文，但**主代理校对阶段会据此提取章节结构并整体丢弃**（不进入最终 EPUB，避免与导航目录重复） |

## 3. 批次标记检查：check-markers（CLI）

全部批次子代理完成后，用脚本逐个检查 `raws/` 下的 `batch_XXX.md`：

```bash
python scripts/check_markers.py book/raws/*.md
```

脚本输出每个文件的标记清单与问题：
- **PAGE**：列出全部页码；检查顺序递增、无缺号、无重复。
- **CH**：列出全部 `<!-- CHn: 标题 -->` 及行号；检查层级是否跳级（作警告）。
- **__**：列出全部行尾/行首 `__` 及行号；检查跨页切口是否成对。

判定：**无问题 → 通过；有问题（PAGE 缺失/乱序、__ 不成对）→ 单独重派该批次子代理**，重跑后复检，直至通过；不重跑全书。若同一批次多次失败，可临时缩小该批次页数（如拆为 5 页）再试，或暂停交由用户处理。

## 4. 批次合并：merge-batches（CLI）

```bash
python scripts/merge_batches.py book/raws -o book/draft.md
```

按批次序号顺序合并所有 `batch_XXX.md` 为 `draft.md`：**保留各批内的 PAGE/CH/__ 标记**（供校对定位、跨批次 `__` 切口衔接）；合并时校验批次序号连续（1..K 无缺）。合并后**规范化行距**：任意两个相邻非空行之间恰好 1 个空行（跨页 `__` 切口连接的行除外，保持切口两部分紧密相连），无连续空行堆积、无缺失空行。

## 5. 草稿标记复查：check-markers（CLI）

```bash
python scripts/check_markers.py book/draft.md
```

合并后对整本草稿复查一次标记：全书画页码连续（无跨批次缺页）、`__` 切口在批次边界正确成对、CH 层级全本连续。复查通过后再进入校对。

## 6. 主代理校对：错别字修正 + 转标题（Agent）

这是 Agent 的灵活任务，不写脚本。基于 `draft.md` 执行：

1. **分块通读**：按批次分批通读 `draft.md`，每块在上下文内完整读完。页数多也不爆上下文。**主要修正识别出来的错别字**，不改变原文、不增删句子。
2. **标记转换**：
   - 删除 `<!-- PAGE XXXX -->` 页定位注释——空白页/无意义页仅含该注释，删除后自然无痕。
   - 去掉跨页行尾/行首的 `__` 连接标记并把两部分合成为一段。
   - 将 `<!-- CH1 -->` / `CH2` / `CH3` / `CH4` 分别转为 `#` / `##` / `###` / `####` Markdown 标题（以此类推）；删除已无用的锚点注释与 `COPYRIGHT_CANDIDATE` 标记。
3. **目录页处理**：原书目录页仅用于核对章节标题与层级，核对完毕后**从正文中移除**。
4. **版权页终审**：逐个确认 `COPYRIGHT_CANDIDATE` 标记的页——确为版权声明则删除；误标（如书名页、献辞页）则保留为正文。
5. **最终检查**：全文无空锚点、无连续空行堆积、无未清除的页眉页脚特征、无 `<!-- PAGE` 残留、无 `__` 残留；h 标题层级连续无跳级。此时产出 `book.md`，是**干净的层级化 Markdown**，h1=全书最高级标题（有部/卷则为部/卷，无则为章），h2、h3 依层级递减。

## 7. md-to-epub：分章与打包（CLI）

```bash
python scripts/md_to_epub.py book/book.md -o book/book.epub \
  --title "书名" --author "作者" --lang zh [--cover book/pages/page_0001.png]
```

脚本行为：

- **切分**：以 h1 为顶级章节切分正文；h2/h3 作为子级保留在章节内。
- **导航目录**：生成 EPUB 导航（NCX 与 nav.xhtml），目录条目与正文 h1/h2 标题一一对应（h3 及以下为章内层级，不进目录），支持点击跳转。
- **封面**（可选）：`--cover <图片>` 指定封面图（PNG/JPG）。实际流程通常用拆页得到的 `page_0001.png`（原书封面页），也可由用户提供其他图片；pandoc 路径用 `--epub-cover-image`，ebooklib 路径用 `EpubImage` 设 `cover-image` 属性，两条路径均生成封面。
- **打包**：优先调用 `pandoc`（Markdown → EPUB）；若 pandoc 缺失，回退用 `ebooklib` 手工组包（目录同样生成）。两种路径都保证阅读顺序与原书一致、已去除的内容不出现。
- 输出 `book.epub`。

## 8. verify：校验（CLI）

```bash
python scripts/verify.py book/book.epub
```

脚本检查并**完整打印校验报告到终端**：

- EPUB 结构合法：`content.opf` / `manifest` / `spine` 完整一致。
- 导航目录存在，且**每条目录条目在正文中都有对应标题**（条目数 ≈ 正文 h1/h2 标题数；封面等非正文页除外）。
- 目录锚点目标可解析（点击可跳转）。
- 正文字段抽查：无 `<!-- CH` 锚点注释残留、无 `COPYRIGHT_CANDIDATE`、无 `<!-- PAGE` 页定位注释残留、无 `__` 连接标记残留。

校验失败时脚本给出具体条目，修复后重跑 `md-to-epub` 与 `verify` 直至通过。

## 常见问题

- **某批次识别失败 / 子代理超时**：用 `check-markers` 检查确认问题，单独重跑该批次子代理（`batch_XXX`），不重跑全书。
- **正文出现公式/表格（罕见）**：本 skill 面向纯文字书籍（如小说），子代理不使用结构标记；若某页含公式表格等非常规内容，如实转录为文字段落，主代理校对时人工核对。
- **某本书需要保留插图**：偏离通用默认，需用户单独确认，并放弃"去除插图"规则。

