OCR 书籍精校与阅读站

SKILL效率
效率 未标注权限 许可证不明确
安装这个技能

End-to-end workflow for scanned-book OCR text. Extract with PyMuPDF+RapidOCR, dictionary-correct recognition errors, correct/keep only real illustrations, AUTO-CROP figures/photos embedded in text-page scans (OpenCV), rebuild the TOC as clickable links, and ship a local continuous-scroll web reader (chapter/method/keyword jump, reading-progress in localStorage). Also ships a static, server-free proofreading web app that shows OCR text + the original page scan + cropped figures side by side for correction.

一句话选型结论

按资料字段整理

OCR 书籍精校与阅读站是效率类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类效率 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_6a069491/pdf-ocr-handler
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

效率
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_6a069491仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791680318
正文体量30.9 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
整理待办与日程

把零散任务收进清单,排出优先级。

批量处理重复任务

把固定动作做成模板,重复执行。

把流程写成清单

把口头流程整理成可复用的步骤。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在效率场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
OCR 书籍精校与阅读站当前 未标注权限 未标注 未标注 未标注 有
Nano PDF 文档 联网访问 ClawHub 市场 424.9 万 OpenClaw 有
自动更新程序 浏览器控制 ClawHub 市场 296.4 万 OpenClaw 有
Gemini 助手 联网访问 ClawHub 市场 254.7 万 OpenClaw 有
word DOCX 文档 联网访问 ClawHub 市场 239.6 万 OpenClaw 有
Excel XLSX 前端 联网访问 ClawHub 市场 227.0 万 OpenClaw 有

SKILL.md 全文

30.9 KB
模型实际读到的说明书原文,来自技能仓库

OCR 书籍精校与阅读站

把扫描书籍的 OCR 结果,从「能看出是什么字」提升到「可以当正式文本读」,并交付本地网页。

v2.1 在 v2 流水线之上,新增两条能力:
- ⑥ 自动图表/照片裁切:当整本书每页都是一张满版扫描图、正文与图表混排时,用 OpenCV 把页内真实的图(曲线图/示意图/照片)抠出来,单独存 figures/,并把 bbox 写回 JSON。
- ⑦ 静态对照校对网页:零后端、单个自包含 HTML,左图(原扫描页)右文(OCR 文本),下方是裁切出的图,支持翻页/跳页/章节目录/搜索/标记问题,供人眼逐页校对。

> 两种书型请选对流水线:
> - 图文分页型(如《88个最高效记忆方法》):有整页纯插图页,用 ③ 插图分类把文本页扫描图归档,阅读站只显插图页。
> - 满版扫描混排型(如《储能原理与技术》):每页都是满版扫描、正文里嵌图,用 ⑥ 裁切把图抠出来,再用 ⑦ 校对网页逐页对照。

何时使用

  • 手头有整本书的扫描 PDF(每页是扫描图、原文件无文字层)
  • 需要批量修正识别错误、补缺字、理顺被打乱的语序
  • 想要一个能对照原扫描页、边读边改并落库的校对器(⑦ 静态网页,或 v1 服务端版),或一个能连续滚动、带目录/方法/关键词跳转、可记录阅读进度的阅读站(⑤)

整体架构(流水线)

扫描PDF ──①抽取──▶ text.json(每页 text + images[满版扫描相对路径])
       ──②校正──▶ 原地校正 text + corrections_log.json(审计)
       ──③插图分类──▶ 仅保留纯插图/照片页,文本页图归档(图文分页型用)
       ──⑥图表裁切──▶ 从满版扫描抠出真实图 → figures/ + JSON.figures(混排型用)
       ──④目录重建──▶ 解析目录页 → data.js.toc(可跳转)
       ──⑤阅读站──▶ index.html + data.js + app.js(连续滚动阅读)
       ──⑦静态校对网页──▶ proofreader.html(自包含,左图右文+裁切图,零后端)
层 做什么 产物
① 抽取 PyMuPDF 取每页嵌入图 + RapidOCR 识别;过滤推广水印;原子写;断点续跑 text.json
② 校正 去前 N 页 + 短语级高置信度词表替换 校正后 text + corrections_log.json
③ 插图分类 纯插图/照片页保留,文本页整页扫描图移入归档 仅真实插图
⑥ 图片裁切 OpenCV OTSU+形态学+连通域,抠出页内真实图/照片 figures/ + JSON.figures
④ 目录重建 解析目录页 / 运行页眉 → 章节+跳转目标 data.js.toc
⑤ 阅读站 连续滚动、首行缩进、目录/方法/关键词跳转、进度持久化 本地网页
⑦ 校对网页 对照原扫描页+裁切图,人工改/标记,回写 JSON 单个 HTML

关键设计:② 可以反复重跑而不会丢失人工成果——若采用「text 不可变 + corrected_text + proofed」模型,重跑时先保留 proofed=true 的页。本次对话为求简单采用了「原地改 text + corrections_log.json 审计」,阅读站/校对网页无需选字段。

环境与解释器(血泪陷阱)

  • 固定并隔离解释器:用 managed Python 3.13 建 venv,全程锁定,不要中途换。

bash C:/Users/wingstan/.workbuddy/binaries/python/versions/3.13.12/python.exe -m venv C:/Users/wingstan/.workbuddy/binaries/python/envs/default C:/Users/wingstan/.workbuddy/binaries/python/envs/default/Scripts/python.exe -m pip install --timeout 120 pymupdf rapidocr-onnxruntime pillow opencv-python-headless numpy

> 本次《储能》对话用的是 Python 3.12(C:\Python312\python.exe),opencv/numpy/rapidocr/pymupdf 装在同目录 pkgs312。两种都行;⑥ 图表裁切只需 opencv-python-headless + numpy,与抽取解释器可共用。

  • Git Bash 下的路径必须用正斜杠:反斜杠会被 shell 吞掉,C:\Users\... 变成 C:Users... 导致 command not found。统一写 C:/Users/... 或 /c/Users/...。

  • pip 偶尔断 / 升级会把自己搞坏:网络不稳加 --timeout;若 pip 损坏用 python -m ensurepip --upgrade 救回,再重装目标包。

  • RapidOCR 模型内置,装完即可用,CPU 友好(每页 ~4–7s)。校验是否加载:from rapidocr_onnxruntime import RapidOCR; RapidOCR()。

  • opencc 仅「繁→简」需要:本次是简体书,未用;繁体书先在抽取前转换。

  • PDF 渲染依赖 PyMuPDF(import fitz):缺它时优雅降级,关闭扫描页对照,其余照常。

  • ⚠️ OpenCV 中文路径陷阱(⑥ 必踩):cv2.imread(path) / cv2.imwrite(path, img) 在含中文的路径上会静默返回 None / 不写文件,且不抛错,极难排查(表现为「0 张裁切 / 读不到图」)。必须改用 cv2.imdecode / cv2.imencode:

```python
import numpy as np, cv2

def imread_cn(path):
# 用 numpy 读字节再 decode,绕过 cv2.imread 的中文路径 bug
with open(path, "rb") as f:
buf = np.frombuffer(f.read(), dtype=np.uint8)
return cv2.imdecode(buf, cv2.IMREAD_COLOR)

def imwrite_cn(path, img):
ok, arr = cv2.imencode(".png", img)
if not ok:
raise IOError("imencode failed: " + path)
with open(path, "wb") as f:
f.write(arr.tobytes())
return True
```

满版扫描图、裁切输出都在中文目录(如 E:/学习/.../extracted/)时,全程用这两个函数,不要碰 cv2.imread/imwrite。

① 抽取(extract_pdf.py 核心)

扫描 PDF 每页是一张嵌入图,无文字层。流程:取每页嵌入图 → RapidOCR 识别 → 过滤页脚水印 → 写出 JSON → 导出该页图为 images/page_NNN.jpg(混排型保留全部页扫描;图文分页型见 ③)。

import fitz, json, re
from rapidocr_onnxruntime import RapidOCR
engine = RapidOCR()
WATERMARK = ("慧眼看", "huiyankan")   # 推广水印,识别时剔除

for i, page in enumerate(doc, start=1):
    imgs = page.get_images(full=True)
    xref = imgs[0][0]
    raw = doc.extract_image(xref)["image"]      # 整页扫描图 bytes
    # 存为满版扫描(混排型:每页都留,供 ⑥ 裁切 / ⑦ 对照)
    open(f"images/page_{i:03d}.jpg", "wb").write(raw)
    # OCR
    result, _ = engine(raw)                      # result: [[box, text, score], ...]
    if result:
        lines = [r[1] for r in result
                 if not any(w in r[1] for w in WATERMARK)]
        text = "\n".join(lines)
    else:
        text = ""
    out["pages"].append({
        "page": i,
        "text": text,
        "images": [f"images/page_{i:03d}.jpg"],   # 满版扫描列表
        "figures": []                              # ⑥ 裁切后填充
    })

断点续跑 + 原子写:每页处理完即把整个 JSON 以 tmp 写出再 os.replace 覆盖,脚本被杀可重跑跳过已完成页(用 page 去重)。

注意:个别页(如末页)可能无嵌入图,直接渲染高分辨率 page.get_pixmap(matrix=fitz.Matrix(2,2)) 补全,保证「页数 ↔ 插图数」一致。

② 规则校正(correct_and_restructure.py)

三件一起做:去前 N 页、词典替换、插图重分类(图文分页型),并产出 corrections_log.json。

REMOVE_FIRST = 3   # 去原 PDF 前3页(封面/版权/目录前页)
CORRECTIONS = [
    ("记亿","记忆"),("印像","印象"),("自已","自己"),("己经","已经"),
    ("商费","商界"),("疲您","疲惫"),("周未","周末"),("知到","知道"),("眼晴","眼睛"),
    ("深人","深入"),("投人","投入"),("若千","若干"),("时侯","时候"),("究竞","究竟"),
    ("毕竞","毕竟"),("身休","身体"),("感概","感慨"),("连想","联想"),("现像","现象"),
    ("部份","部分"),("形像","形象"),("对像","对象"),("凭藉","凭借"),("决对","绝对"),
    ("上牛","上午"),("下牛","下午"),("中牛","中午"),("膏盲","膏肓"),("慷概","慷慨"),
    ("色采","色彩"),("精采","精彩"),("布骤","步骤"),("关连","关联"),("连系","联系"),
    # …… 共约 60 条短语级高置信度混淆对
]
# 按长度降序,优先替换较长短语,避免被短词部分命中
CORRECTIONS.sort(key=lambda x: -len(x[0]))
COMP = [(w, re.compile(re.escape(w)), r) for w, r in CORRECTIONS]

log = []
for pg in pages:
    t = pg["text"]
    if not t.strip(): continue
    new = t
    for w, rx, r in COMP:
        if w in new:
            idx = new.find(w)
            ctx = new[max(0,idx-12):idx+len(w)+12].replace("\n"," ")
            log.append({"page": pg["page"], "wrong": w, "right": r, "context": ctx})
            new = new.replace(w, r)
    pg["text"] = new

# 去前 N 页 + 重新顺序编号
pages = [p for p in pages if p["page"] > REMOVE_FIRST]
for i, p in enumerate(pages, start=1): p["page"] = i

词典选型原则:只收「wrong 几乎从不作为正确词出现」的短语级混淆对,避免误改正确用词(详见下方「四条铁律」)。输出 corrections_log.json 供逐条复核。混排型(⑥)通常不做 ③ 的插图归档,保留 images 作对照底图。

数据结构约定

源:text.json(混排型推荐,满版扫描全留 + 裁切图)

{
  "source_file": "储能原理与技术 扫描版.pdf",
  "page_count": 310,
  "pages": [
    {
      "page": 1,
      "text": "OCR 文本…",
      "images": ["images/page_0001_img_001.jpeg"],
      "figures": [
        {"path": "figures/page_0001_fig_001.png", "bbox": [72, 152, 297, 377], "caption": null}
      ]
    }
  ]
}
  • images:列表,存该页满版扫描相对路径(混排型每页都有,供 ⑦ 对照)。
  • figures:⑥ 裁切后填充,元素为 {path(相对), bbox[x1,y1,x2,y2](原图像素), caption(可空)}。

变体(图文分页型,image 单数)

{ "page": 1, "text": "OCR 文本…", "image": "images/page_001.jpg" }

文本页 image 置空、整页扫描移入 images/_archive_textpages/(见 ③)。两种模型皆可,按书型取舍;⑥⑦ 流水线统一用上面的列表模型。

内嵌:data.js(避免 file:// 跨域,阅读站直接 <script> 引入)

window.BOOK_DATA = {
  "meta": { "title": "…", "author": "…", "page_count": 310 },
  "chapters": [ { "num": 1, "page": 10, "title": "第1章 绪论…" } ],
  "methods":  [ { "name": "联想记忆法", "page": 26 } ],
  "tocPages": [4,5,6,7,8],
  "toc": [ { "title": "第1章 …", "target": 10,
             "methods": [ { "name":"…", "desc":"…", "page_label":2, "target":10 } ] } ],
  "pages": [ { "page": 1, "text": "…", "images": ["images/page_001.jpg"], "figures": [] } ]
};

推荐模型(健壮,支持 idempotent 重跑):保留原始 OCR 于 text(只读基准),校正结果写 corrected_text,人工已校页标 proofed=true。阅读站读 corrected_text || text。本次对话为简洁采用「原地改 text + corrections_log.json 审计轨迹」,二者皆可。

③ 插图分类规则(图文分页型)

  • 保留:校正后 text 仍为空(无文字)的页 = 纯插图/照片页。
  • 归档:文本页的整页扫描图(只是内容扫描,并非插图)→ image 置空,图片移入 images/_archive_textpages/(可恢复,不永久删)。
  • 这样阅读站只在真正插图页显示图,文本页不显示「整页扫描」。
  • 混排型(⑥⑦)跳过本步,保留全部 images 作对照底图。

⑥ 自动图表/照片裁切(混排型核心)

目标:从满版扫描图中,把页内真实的图(曲线图、示意图、框图、照片)抠出来,过滤掉正文段落、标题、页眉页脚。输出 figures/page_{page:04d}_fig_{idx:03d}.png,并把 {path,bbox,caption} 写回 text.json 的 figures。

算法(crop_figures_v2.py 核心)

两路检测 + 合并 + 抑制文本:

import cv2, numpy as np, json

def detect_figures(gray):
    h, w = gray.shape
    page_area = h * w
    # —— 行图画(线图/框图):OTSU 二值 + 形态学闭 → 连通域
    _, bin_ = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    k_close = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
    bin_ = cv2.morphologyEx(bin_, cv2.MORPH_CLOSE, k_close, iterations=2)
    bin_ = cv2.morphologyEx(bin_, cv2.MORPH_OPEN, (3,3), iterations=1)
    # —— 照片(连续调):灰度落在 [30,220] 的掩膜
    photo = cv2.inRange(gray, 30, 220)
    photo = cv2.morphologyEx(photo, cv2.MORPH_CLOSE, (25,25), iterations=2)
    photo = cv2.morphologyEx(photo, cv2.MORPH_OPEN, (5,5), iterations=1)

    # 行高估计(用 OTSU 连通域里「瘦高、面积适中」的块)
    n, _, stats, _ = cv2.connectedComponentsWithStats(bin_, 8)
    chs = [s for s in stats[1:] if 20 <= s[cv2.CC_STAT_AREA] <= 2500
           and 12 <= s[cv2.CC_STAT_HEIGHT] <= 80
           and 0.25 <= s[cv2.CC_STAT_WIDTH]/max(1,s[cv2.CC_STAT_HEIGHT]) <= 8]
    line_height = int(np.median([c[cv2.CC_STAT_HEIGHT] for c in chs]) * 2.3) if chs else int(h*0.03)

    cand = []   # 候选框 [x1,y1,x2,y2]
    # 行图画块
    n, _, stats, _ = cv2.connectedComponentsWithStats(bin_, 8)
    for s in stats[1:]:
        x,y,bw,bh,a = s[:cv2.CC_STAT_AREA+1]
        fill = a / (bw*bh)
        aspect = bw/bh
        if fill < 0.08 and aspect > 1.4 and a > page_area*0.018:   # 文本块,丢弃
            continue
        if fill < 0.08 and a > page_area*0.12 and bh > line_height*2.5:  # 大标题,丢弃
            continue
        if fill < 0.09 and aspect > 1.8 and bh < line_height*1.8:  # 单行文字,丢弃
            continue
        cand.append([x, y, x+bw, y+bh])
    # 照片块
    n, _, stats, _ = cv2.connectedComponentsWithStats(photo, 8)
    for s in stats[1:]:
        x,y,bw,bh,a = s[:cv2.CC_STAT_AREA+1]
        if a < page_area*0.002 or bw/bh > 6 or a > page_area*0.30 or bh > h*0.45 or (a/(bw*bh)) < 0.12:
            continue
        cand.append([x, y, x+bw, y+bh])
    return merge_boxes(cand, h, w, page_area, line_height)

合并与最终过滤(merge_boxes):

def merge_boxes(cand, h, w, page_area, line_height):
    merge_gap   = int(min(h,w)*0.018)   # 近邻直接并
    fig_gap     = int(min(h,w)*0.065)   # 同图元素间距上限
    cand.sort(key=lambda b:(b[1], b[0]))
    merged = []
    for b in cand:
        hit = None
        for m in merged:
            # 行重叠足够 + 纵向间距小 → 并
            ov = min(b[2],m[2]) - max(b[0],m[0])
            if ov > 0.40*min(b[2]-b[0], m[2]-m[0]) and b[1]-m[3] < fig_gap:
                hit = m; break
            if b[1] <= m[3]+merge_gap and b[0] < m[2] and b[2] > m[0]:
                hit = m; break
        if hit:
            hit[0]=min(hit[0],b[0]); hit[1]=min(hit[1],b[1])
            hit[2]=max(hit[2],b[2]); hit[3]=max(hit[3],b[3])
        else:
            merged.append(b[:])
    # 最终过滤
    out = []
    for (x1,y1,x2,y2) in merged:
        bw, bh = x2-x1, y2-y1
        if bw*bh < page_area*0.55:                    # 太小
            continue
        if bw/bh > 5.5:                               # 太扁(多为分隔线/页眉)
            continue
        if bw/bh > 2.2 and bh < line_height*1.5:      # 宽而矮 → 正文行带,丢
            continue
        out.append([x1,y1,x2,y2])
    return out

裁剪时按页边距裁掉装订/扫描黑边,再用 imwrite_cn 存盘:

mt, mb, ml = int(h*0.050), int(h*0.030), int(w*0.025)
for idx, (x1,y1,x2,y2) in enumerate(figs, 1):
    x1=max(ml,x1); x2=min(w-ml,x2); y1=max(mt,y1); y2=min(h-mb,y2)
    crop = img[y1:y2, x1:x2]
    imwrite_cn(f"figures/page_{page:04d}_fig_{idx:03d}.png", crop)
    page_json["figures"].append({"path": f"figures/page_{page:04d}_fig_{idx:03d}.png",
                                 "bbox":[x1,y1,x2,y2], "caption": extract_caption(text)})

调参口诀(recall ↔ precision)

  • 漏照片(如某页整页照片被当成大文本块丢弃):放宽面积上限 page_area*0.30、高度上限 bh>h*0.45(本次 page 17 即此修)。照片掩膜阈值 inRange(gray,30,220) 偏保守,过曝/过暗照片可适当缩窗。
  • 文本段落误判为图:收紧 fill(0.09→0.08)、加 text-block(aspect>1.4 and area>page_area*0.018)与 big-text(area>page_area*0.12 and bh>line_height*2.5)抑制;宽扁短带 bw/bh>2.2 and bh<line_height*1.5 直接丢。
  • 整页被并成 1 个图:闭运算核从 35×35 降到 15×15、迭代 2;合并间距 fig_gap 不要过大。
  • 跑完后统计每页裁切数 + bbox 面积占比,人工抽 5–10 页判召回/精度,再微调,不要盲调。

运行

cd E:/.../extracted
C:/Python312/python.exe crop_figures.py text.json figures --start 1 --end 310
# 输出:Done. Total crops: 703 / Pages with figures: 284

④ 目录重建(build_data.py 的 toc 部分)

目录页(如本书 pages 4–8)原文含「目录/Contents」页眉与 OCR 错乱页码。解析为结构化、可跳转的 TOC:

TOC_PAGES = [4,5,6,7,8]
GARBAGE = set("V¥N√《入X八九十了人体")   # 页码前的 OCR 噪声字符

def clean_page(tok):
    s = tok.strip()
    if s and not s[0].isdigit() and s[0] in GARBAGE: s = s[1:]
    m = re.search(r"(\d+)\s*$", s)
    return int(m.group(1)) % 1000 if m else None   # 去多余前导1(1127→127)

# 跳转目标:优先精准名称匹配内容方法;否则「印刷页码 + 偏移」回退
name2page = {m["name"]: m["page"] for m in methods}   # 方法检测已排除目录页
offset = chap_by_num.get(1, 9) - 1                    # 第一章正文≈印刷页1

# 章节范围校验:把 OCR 错位的页码拉回合理区间(142→42、470→70)
def clean_page_label(num, cnum):
    lo = chap_by_num.get(cnum,1)-offset-4
    hi = chap_next.get(cnum, TOTAL)-offset+4
    while num > hi and num >= 100: num = int(str(num)[1:])
    return num

要点:
- 方法检测时排除目录页本身,否则会把目录条目误判为书中方法(侧栏「记忆法」会脏)。
- 页码校正:剥离噪声符 → 按所在章节的页码范围把越界数字「去掉首位数」拉回(如 142→42、470→70、124→24)。
- 跳转目标:条目名与内容方法精确匹配最佳;描述性条目(如「衡量记忆力好坏的标准」)用 印刷页码 + offset 估算,落在该章正确区间即可。

运行页眉分章(混排型 / 无目录页时也适用)

满版扫描书常在每页顶部印「第N章 标题」运行页眉,且相邻页重复。据此分章:

import re
pat = re.compile(r'第([一二三四五六七八九十百零\d]+)章')
CN = {'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'百':100,'零':0}
def cn2int(s):
    # 简版:处理「十/十一/二十一」等;数字直接 int
    if s.isdigit(): return int(s)
    n=0,unit=0;  # 完整实现见脚本
    ...

runs = []                 # (label, start_page, run_len)
cur=None; cnt=0
for pg in pages:
    m = pat.search(pg["text"] or "")
    lab = m.group(0) if m else None
    if lab == cur: cnt += 1
    else:
        if cur is not None: runs.append((cur, start, cnt))
        cur, start, cnt = lab, pg["page"], 1
if cur is not None: runs.append((cur, start, cnt))

# 只把「连续出现≥2页」的标签认定为章节起始,单页闪现的 OCR 错识标签自动忽略
chapters = [{"label": l, "first_page": s} for (l, s, c) in runs if c >= 2 and l]

> 经验:某页页眉被错识成「第5章」而前后页都是「第1章」时,该标签只出现 1 页,c>=2 直接滤掉,不会污染目录。

⑤ 阅读站(continuous-scroll reader)

index.html + styles.css + app.js + data.js,纯前端、零构建、可 file:// 直接打开。

核心:splitParagraphs 把 OCR 物理行合并为自然段落(顺带修掉「难/度」「深/入」这类被扫描行断开的词),并首行缩进。

var TERM = /[。!?!?;;::“””]$/;
function splitParagraphs(text) {
  if (!text || !text.trim()) return [];
  var lines = text.split(/\r?\n/), paras = [], cur = [];
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i].trim();
    if (line === "") { if (cur.length) { paras.push(cur.join("")); cur = []; } continue; }
    cur.push(line);
    if (TERM.test(line) && i < lines.length - 1) { paras.push(cur.join("")); cur = []; }
  }
  if (cur.length) paras.push(cur.join(""));
  return paras.filter(function (p) { return p.trim(); });
}

阅读站功能:
- 连续滚动:一次 DocumentFragment 建完全书 DOM(约数百段 + 8 图),无逐屏虚拟化、无 scroll 重计算,避免卡顿。
- 跳转:目录/方法/关键词点击 → el.scrollIntoView。目录条目(.toc-item / .toc-chap-head)带 data-target;关键词搜索列页码+上下文片段。
- 目录重排:pages 4–8 渲染为正式目录(章标题 + 「方法名 … 页码 描述」可点行),其余目录页作「(目录·续)」占位。
- 阅读进度:IntersectionObserver 进视口即记 readSet;顶栏进度条 + 目录/方法前 ✓;localStorage 持久化(STORE_KEY 随大改升版本,避免旧页码串味);刷新自动恢复到上次位置。
- 插图:loading="lazy" 懒加载;顶栏「隐藏插图」全局开关(.no-image)。
- 章标题:章起始页渲染 第N章 … 大标题(h2.chapter-head),并打 chap-N 锚点。
- 裁切图(混排型):figures 非空时在段落末插 <figure> 缩略图灯箱,呼应 ⑥。

⑦ 静态对照校对网页(零后端)

用途:人眼逐页校对——左看原扫描页、右读 OCR 文本、下看 ⑥ 裁出的图,发现错字/漏字/图漏裁可在页面标记,结果存 localStorage(可选回写 JSON)。

为什么不用服务端:纯静态 file:// 直接双击打开,无需起服务、无跨域。数据用 <script type="application/json" id="bookdata">…</script> 内嵌(与 data.js 同理,规避 file:// 的 fetch 跨域)。

生成器(build_proof_html.py)要点:

import json
data = json.load(open("text.json", encoding="utf-8"))
raw = json.dumps(data, ensure_ascii=False)
raw = raw.replace("</", "<\\/")          # 防 </script> 截断
html = """<!doctype html><html lang="zh"><head><meta charset="utf-8">
<title>校对 · {title}</title><style>{css}</style></head><body>
<header>…翻页/跳页/搜索…</header>
<main>
  <section id="scan"><img id="pageimg" loading="lazy"></section>
  <section id="text"><pre id="pagetext"></pre></section>
  <section id="figs"></section>
</main>
<script type="application/json" id="bookdata">{data}</script>
<script>{appjs}</script></body></html>""".format(title=..., css=CSS, data=raw, appjs=APPJS)
open("proofreader.html", "w", encoding="utf-8").write(html)
  • HTML 放在 extracted/ 内,相对路径 images/... 与 figures/... 才能直接解析。
  • 渲染单页:images[0] 作满版对照图,figures 渲染为缩略图画廊(点击放大)。
  • 导航:上一页/下一页/页码跳转;← → 键盘翻页;顶部「章节」下拉(用 ④ 的运行页眉分章结果)。
  • 搜索:在 OCR 文本里搜词 → 跳到首个命中页并高亮。
  • 标记问题:每页「✓ 已校 / ⚠ 有问题」按钮,备注写 localStorage[page],顶栏汇总「已校 X/310,标记 Y」;可一键导出标记页清单(CSV/JSON)。
  • 回写(可选):导出的标记清单交给 correct_and_restructure.py 或人工在 text.json 上改。

> 本次《储能》对话未生成该 HTML(裁剪已完成,703 图 / 284 页有图),生成器脚本即上方模板,按需 python build_proof_html.py 即可产出 proofreader.html。

写规则的四条铁律(金科玉律,适用于 ② 与可选校对器)

1. 用足够长的上下文,别用单字

单字替换会误伤。("症疾","疟疾") 会把「重症疾病」改坏。加长上下文或用负向断言:

text = re.sub(r'(?<!重)症\s*疾', '疟疾', text)   # 排除「重症疾病」
text = re.sub(r'(?<!胃)肠痛', '肠痈', text)      # 排除穴位「胃肠痛」

2. 加规则前先查原始 text

曾写 ("支气管哮","支气管哮喘"),但原文本来就是「支气管哮喘」,规则又补「喘」,全书 16 处变成「支气管哮喘喘」。下手前先确认原文缺没缺字:

python scripts/scan_tokens.py book.json 支气管哮 --field text

3. 缺字和语序错乱,用「整句重建」

规则引擎搞不定单字插入,但可以整片替换。把缺字/乱序片段连同确定上下文一起换掉:

("饮是湿温证候", "小便黄赤为湿热证候。"),
("应考虑疾", "应考虑痢疾"),
("人参、桃各2钱", "人参、桃仁各2钱"),

4. 垃圾行直接置空

OCR 在插图、页眉页脚常吐无意义字串,映射到空串:

("贝量生未都曼业", ""), ("人第治业事斯黄义克会山命病主会普", ""),

排版核心:硬换行 reflow

OCR 按扫描行硬换行,直接渲染完全没法读。两种合并策略:

  • 校对器(精确,满行判定):扫描行排满才换行,只有明显短于满行且以句末标点收尾的,才是段末。取本页行长 80 分位数为「满行宽度」:

javascript const lens = raw.map(l=>l.trim().length).filter(n=>n>0).sort((a,b)=>a-b); const fullW = lens.length ? lens[Math.floor(lens.length*0.8)] : 24; const FULL_T = Math.max(11, fullW - 3); if (END_PUNCT.test(buf) && !OPEN_TAIL.test(buf) && prevLen < FULL_T) flush();

  • 阅读站(轻量,句末标点):本次对话用 splitParagraphs(见上),按句末标点切段,足够流畅且代码极简;对「难/度」类断词一并修复。若书有长条目/标题行,需保护结构(条目行 (1) 1. ① 【按】、标题行 第三章 〔治疗〕)独立成块。

验证陷阱:假阳性(永远看上下文)

扫描「残留坏词」时大部分报警是假的——替换后的正确长串仍包含原坏词作子串:

扫描到 实际内容 判定
不良性水肿 营养不良性水肿 ✅ 假阳性
一对立统一关系 这一对立统一关系 ✅ 假阳性
氟骨 氟骨症 ✅ 假阳性

所以永远不要只看命中数,必须看上下文。scan_tokens.py 默认输出前后 20 字上下文。判定顺序:看上下文 → 确认是正确长串子串 → 标假阳性放过;只有孤立出现的才是真漏改。

⑥⑦ 交付前检查

# 页数完好、插图/裁切路径解析、图表数合理
python -c "
import json, os
d=json.load(open('text.json',encoding='utf-8'))
pages={p['page']:p for p in d['pages']}
print('pages:', len(d['pages']))
miss_img=[p['page'] for p in d['pages'] if p.get('images') and not os.path.exists(p['images'][0])]
miss_fig=[f['path'] for p in d['pages'] for f in p.get('figures',[]) if not os.path.exists(f['path'])]
tot_fig=sum(len(p.get('figures',[])) for p in d['pages'])
print('满版扫描缺失:', miss_img[:5], '...共', len(miss_img))
print('裁切图缺失:', len(miss_fig), '| 总裁切数:', tot_fig)
# bbox 合理性抽检
bad=[(p['page'],f['bbox']) for p in d['pages'] for f in p.get('figures',[]) if not all(isinstance(v,int) and v>=0 for v in f['bbox'])]
print('bbox 异常:', bad[:5])
"

阅读站另查:data.js 解析无误、images/ 仅留真实插图、进度键版本正确、本地服务 curl 全资源 200。校对网页另查:双击 proofreader.html 能开、翻页图片 200、标记可存 localStorage。

参考

  • references/pitfalls.md —— 踩过的坑与判定方法详解(v1)
  • references/reader-app.md —— 校对器架构、API、前端结构说明(v1)
  • 本次对话产物示例(混排型 + ⑥⑦):《储能原理与技术》E:\学习\explore\技能\储能原理与技术\extracted\(含 text.json / images/ / figures/ 703 张 / figures_manifest.json)
  • 早期示例(图文分页型 + ⑤):《88个最高效记忆方法》E:\学习\explore\技能\清华北大状元告诉你的88个最高效记忆方法 谭兴茂 北京理工大学 扫描版_提取\(含 text.json / data.js / index.html / app.js / corrections_log.json / images/)

脚本清单

脚本 用途
scripts/extract_pdf.py ① 抽取:PyMuPDF 取图 + RapidOCR,断点续跑、原子写
scripts/correct_and_restructure.py ② 校正:去前 N 页 + 词典替换 + 插图重分类 + 审计日志
scripts/crop_figures.py ⑥ 图表裁切:OpenCV OTSU+形态学+连通域,抠图写回 JSON
scripts/build_data.py ④ 目录重建:章节/方法检测 + TOC 解析(含运行页眉分章),生成 data.js
scripts/build_proof_html.py ⑦ 校对网页:内嵌 JSON 生成自包含 proofreader.html
assets/app.js assets/index.html assets/styles.css ⑤ 阅读站:连续滚动前端
scripts/rule_engine.py(v1) 阶段1 规则校正引擎模板
scripts/dump_pages.py(v1) 按页范围导出文本供审阅
scripts/scan_tokens.py(v1) 全书坏词扫描(带上下文,假阳性判定用)
scripts/reader_server.py(v1) 可选服务端校对器后端(对照原扫描页,回写 JSON)

常见问题

5 问
答案由本站依据条目字段整理
OCR 书籍精校与阅读站怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_6a069491/pdf-ocr-handler。安装方式为未标注。装完刷新技能列表或重开客户端。

OCR 书籍精校与阅读站支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

OCR 书籍精校与阅读站权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

OCR 书籍精校与阅读站收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

OCR 书籍精校与阅读站用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。