Files
hermes-skills/skills/legal/contract-portfolio-analysis/references/scanned-pdf-ocr-recipe.md
T

19 KiB

纯扫描件合同 OCR 配方(image-only PDF → 可读文本)

适用:南通新东方多数校区合同是纯扫描件——pdftotext 文字层字符数 ≈ 0(个位数), 必须 OCR 才能读。人民中路、跃龙路实证此配方干净好用,优先于 deepseek_ocr 路径。

配方(tesseract 引擎,中英混排)

# 1) ocrmypdf 给扫描件加文字层(force-ocr 强制重做,image-dpi 300 提清晰度)
ocrmypdf -l chi_sim+eng --force-ocr --image-dpi 300 租赁.pdf 租赁-ocr.pdf
# 2) pdftotext -layout 抽文本(-layout 保留表格的列对齐,读租金表/费用表更可靠)
pdftotext -layout 租赁-ocr.pdf 租赁-OCR.md
  • -l chi_sim+eng:中文简体+英文,合同里身份证号/账号/英文条款都能认。
  • 为什么用 ocrmypdf 而非 deepseek/裸 tesseract:① 一步产出带文字层的 PDF(后续 x2t 渲染、人工查阅都能用);② pdftotext -layout 出来的文本保留空间布局,财务表格的列不会糊成一行;③ tesseract 对扫描合同的正文条款识别率足够高。
  • 大文件后台跑:10MB+ 的扫描 PDF(如跃龙路租赁 10.9MB)OCR 要几分钟,写脚本 terminal(background=true, notify_on_complete=true),发出后并行做别的(见 SKILL Pitfall 4)。
  • 验证:OCR 后 wc -c 租赁-OCR.md 字符数应从 0 跳到**数千数万**(人民中路租赁 8 页→31420 字符、物业 3 页→6111;跃龙路 6+6 页同量级)。仍是个位数=OCR 没生效,回查 ocrmypdf 报错。

两个必防的 OCR 失真(扫描件通病)

① 财务表格"大写金额"几乎必乱码 → 以阿拉伯数字为准

正文条款 OCR 可读,但租金表里的中文大写金额("壹拾壹万玖仟…")常被识别成乱码(SRSAUSRSAUT SAAR 之类)。处置:

  • 关键金额一律以阿拉伯数字栏为准(如 119,190.75),大写乱码忽略,不照抄乱码进表
  • 数额存疑/阿拉伯数字也不清 → 回原图 vision_analyze 核,或走金额数学交叉验证(不含税×(1+税率)=含税,见 SKILL「4d/数学交叉验证」)。

② 抬头/公司名乱码 → 裁高清局部图 vision 逐笔辨认 + 多处交叉核

扫描件首页抬头(甲方/乙方公司全称)常因字号小/印章压字而 OCR 乱码,且这是梳理表的关键基础项,不能蒙。处置:

pdftoppm -png -f 1 -l 1 -r 400 租赁.pdf hd_p1      # 400dpi 高清渲染首页
# python: PIL 裁抬头区(高度~4%-16%) → resize 宽1200 → 存 jpg quality≥90 (<400KB 防 vision 超时)
  • 把抬头局部图喂 vision_analyze,要求逐字辨认、看不清就明说哪个字看不清、禁止猜测填充
  • 多处交叉核:公司名在合同里通常出现 3+ 处(抬头、第三条收付款户名、落款、骑缝章),用整页+高清局部两次独立辨认互证;与旧梳理表/兄弟合同的记法对撞——不一致就是该名称 OCR 不稳的信号,必须核到底。
  • 人民中路实证:旧表(0622)甲方记"南通琳大鞍房屋…",本次 vision 高清逐笔辨认为"南通森大蒂房屋建设开发有限公司"("森"=品字三木,确非"琳"),纠正了旧表的误识。OCR 与旧表都可能错,回高清原图 vision 核才是真值。

③ tesseract 空白输出救援:二值化(binarization)回退(2026-06-26 通州金鹰实证)

症状

ocrmypdf 整份跑完,pdftotext 抽出来的文本只有前几页、后几页完全空白(80 字节的 \f 换页符)。拆出空白页单独 tesseract 直接读 PNG 也一字不输出——但 PNG 文件大小正常(2-3MB),numpy 统计非白像素 1000 万+,页面有内容、只是 tesseract 读不出来

根因

扫描件对比度低/底色不均/噪点多,tesseract 默认的灰度处理在纹理复杂的旧扫描件上找不到文字边界。

正解:二值化(threshold=128)→ tesseract

from PIL import Image
img = Image.open('page.png')
gray = img.convert('L')                           # 灰度
bw = gray.point(lambda x: 0 if x < 128 else 255, '1')  # 阈值 128 二值化
bw.save('page_bin.png')
tesseract page_bin.png stdout -l chi_sim+eng

二值化后 tesseract 能正常识别,输出与正常 OCR 页面同质量。

必做步骤

  • pdftoppm -r 200 -png 渲染空白页 → 二值化 → tesseract(通州金鹰 p4-p8 五页全救回)
  • 不要增加二值化阈值试错——128 是标准阈值,低对比度扫描件用 128 即可;若 128 仍不输出,优先怀疑页面本身无文字(检查 numpy 非白像素数),而非阈值问题
  • 二值化后 OCR 出来的文本与正常 ocrmypdf 产出同质量,直接合并使用

④ 特定乱码字段核实:tesseract → browser_vision 降级链(2026-06-29 解放中路实证)

问题场景

OCR/tesseract 能读出大部分合同文本,但特定字段仍然乱码(如"壹"被读成"过"、数字被吃0、条款号后的填空值模糊)。需要 vision 核实具体字符,但 vision_analyze 反复超时。

诊断:fitz 文字层检测

import fitz
doc = fitz.open('合同.pdf')
for i in range(doc.page_count):
    text = doc[i].get_text()
    if not text.strip():
        print(f'Page {i+1}: 纯扫描页(无文字层)')
    else:
        print(f'Page {i+1}: {len(text)} chars')

纯扫描件 fitz.get_text() 返回空字符串 → 必须先 OCR。OCR 后的 PDF 有文字层但部分字符乱码 → 进入 vision 核实。

vision_analyze 超时问题

vision_analyze 对合同页面图片(即使压缩到 325KB/400×565px 的 72 DPI JPEG)连续超时 5 次(2026-06-29 解放中路实证)。全页 300 DPI PNG(2.5MB)更不用说。不要反复重试 vision_analyze——它对这个场景不可靠。

正解:tesseract 全页 + browser_vision 局部裁图

第一层:tesseract 全页 OCR(已做,处理 90% 文本)

# 300 DPI 渲染每页 → tesseract 逐页读
python3 -c "
import fitz
doc = fitz.open('合同.pdf')
for i in range(doc.page_count):
    pix = doc[i].get_pixmap(matrix=fitz.Matrix(300/72, 300/72))
    pix.save(f'page_{i+1}.png')
"
tesseract page_N.png stdout -l chi_sim+eng --psm 6

tesseract 对中文合同正文条款识别率高(解放中路第六条完整读出、第十二条大部分可读)。

第二层:browser_vision 局部裁图(处理 tesseract 仍乱码的具体字符)

from PIL import Image
# 1. 打开 300 DPI 页面图
img = Image.open('page_3.png')  # 2481x3510
w, h = img.size
# 2. 裁剪目标区域(如第12.1条第4项,约在页面25-35%高度)
crop = img.crop((100, int(h*0.25), w-100, int(h*0.45)))
# 3. 缩放到 600-800px 宽,存 JPEG quality=80
crop_resized = crop.convert('RGB').resize(
    (600, int(crop.height * 600 / crop.width)), Image.LANCZOS)
crop_resized.save('crop_target.jpg', 'JPEG', quality=80)
# 4. browser_navigate file:///tmp/xxx/crop_target.jpg
# 5. browser_vision 问具体问题(如"第4项中'拖欠租金累计达几个月的'数字是多少")

关键要点

  • browser_vision 和 vision_analyze 是不同的后端——browser_vision 通过浏览器截图+视觉模型,对小图片处理更稳定,本 session 一次成功
  • 裁图要精准定位目标条款区域,不要裁全页(太大)也不要裁太窄(缺上下文)
  • 问题要具体:"第4项中拖欠租金累计达几个月的数字是什么"比"请读出全部文字"效果好
  • 中文大写数字(壹/贰/叁)在扫描件中常被 tesseract 误识为形近字,必须 vision 确认
  • 文件大小控制在 50-100KB(JPEG quality=75-80, 600px 宽),避免超时

完整降级链总结

fitz.get_text() 空 → ocrmypdf 全篇 OCR → pdftotext 取文本
  ↓ tesseract 能读的 → 直接用
  ↓ tesseract 乱码的具体字段 → 裁区域 + browser_vision
  ↓ browser_vision 也超时 → 如实告诉 Maggie,请她核对原件
  ↓ 绝不能 → 用旧表数据填 + 手动创建 checkpoint(Pitfall 39)

⑤ 统一社会信用代码 OCR 乱码修复 + 网络交叉验证(2026-06-29 通大附+凤凰文化实证)

问题场景

OCR 把统一社会信用代码中的英文字母部分读乱(如 MA1NAEBX26MA INAEBX26,数字间插入空格;MADQRFT66P 被完整读出但无法确认是否正确)。ocr-integrity-check.py 将 3+ 连续大写字母匹配为"公司名乱码"——但信用代码中的字母部分是合法的。

ocr-integrity-check.py 修复(已落地 2026-06-29)

脚本增加信用代码排除逻辑:检测匹配到的字母序列前后是否有数字,有则判定为信用代码的一部分并跳过。

网络交叉验证技巧

OCR 信用代码不完整时,用企查查/天眼查 web search 验证:

web_search: "公司全称" 统一社会信用代码

已验证案例:

  • 南通青创企业管理咨询有限公司:91320600MA1NAEBX26(企查查)
  • 南通新东方教育科技有限公司:91320602MADQRFT66P(校验位验证通过)
  • 南通业玖物业管理有限责任公司:企查查未查到(可能是小公司或名称细微差异),但 tesseract 重跑清晰读出

信用代码校验位验证

weights = [1,3,9,27,19,26,16,17,20,29,25,13,8,24,10,30,28]
chars = '0123456789ABCDEFGHJKLMNPQRTUWXY'
code_map = {c:i for i,c in enumerate(chars)}
total = sum(code_map[code[i]] * weights[i] for i in range(17))
check = 31 - (total % 31)
expected = chars[check] if check < len(chars) else '?'
# expected == code[17] → 校验通过

⑥ 签名页/签章区域乱码处理(2026-06-29 多校区实证)

问题场景

合同最后 1-2 页(签名盖章页)OCR 产出大量无意义英文字母序列(AAALIVBREERUEANON 等),是印章/手写签名/骑缝章被 OCR 引擎误识别的产物。

处理方式

import re
# 替换 3+ 连续大写字母为 [签章] 占位符
lines[idx] = re.sub(r'[A-Z]{3,}', '[签章]', lines[idx])

判断标准

  • 出现在签名页(通常最后 1-2 页)
  • 上下文含"甲方签字""乙方盖章""日期"等关键词
  • 这些区域不含合同实质条款,替换后不影响完整性检查
  • 替换后 ocr-integrity-check.py 不再报这些位置的问题

⑦ vision 全不可用时的 delegate_task 降级(2026-06-29 跃龙路实证)

问题场景

vision_analyze 连续超时 7+ 次(即使图片压缩到 119KB JPEG),browser_vision 也间歇性超时。所有视觉工具在同一 session 中同时不可用。

正解:delegate_task 批量分发 OCR 核实

delegate_task(tasks=[
    {"goal": "Read scanned contract page and transcribe ALL text. Focus on: ...",
     "toolsets": ["vision", "file"]},
    # 最多 3 个并行
])
  • subagent 的 vision 后端可能与父 agent 不同——同一时段父 agent vision 全挂,subagent 有 2/3 成功读取(跃龙路实证:task 1 超时但 task 2、3 成功)
  • 混合策略:给每个 subagent 不同的页面 + 具体的提取目标(不要笼统"读全文"),提高单次成功率
  • subagent 也超时时的兜底:用已有 tesseract OCR 输出 + 上下文推理重建文本,标注哪些段落是推断而非 vision 确认的

关键要点

  • delegate_task 的 vision 调用走独立的模型实例,timeout 行为不总与父 agent 一致
  • 把最关键的乱码页(租金表、签名页、违约金条款)优先分配给 subagent
  • 返回结果里 subagent 会自报"vision failed, used OCR instead"——检查 status 和 summary,不能假设全部成功

⑧ 单位后缀误读(天→月、年→月等)——数学交叉验证必抓(2026-07-01 凤凰文化实证)

问题场景

OCR 将"0.4元/㎡/"误读为"0.4 a/R 元/㎡/月"——单位"天"被吞掉或变成乱码,导致后续模版比对报告错误地认为物业费(0.4元/月)与租金(12.167元/月)是两个不同标准。实际上:0.4元/天 × 365÷12 = 12.167元/月,两者完全一致。

根因

扫描件中"天"字笔画简单(仅3画),在低质量扫描+OCR识别中极易丢失或被误读为符号碎片(如"a/R")。类似地,"月""年"等单位后缀也可能被吃掉。

检测方法:数学交叉验证

合同中单价通常后面紧跟一个月总额表格。验证公式:

stated_unit_price = 0.4   # OCR 读出的数字
area = 562                # 面积
table_monthly = 6837.854  # 表格中月费

# 如果是 元/㎡/月:
if_monthly = stated_unit_price * area  # 0.4 × 562 = 224.8 ≠ 6837.854 → 不匹配!
# 如果是 元/㎡/天:
if_daily = stated_unit_price * area * 365 / 12  # 0.4 × 562 × 30.417 = 6837.85 ✓ 匹配!

匹配失败 = 单位被误读。必须回原图 vision 确认真实单位。

铁律

  • 凡 OCR 读出单价+单位(元/㎡/月、元/㎡/天、元/吨等),必须用表格中的月总额做交叉验证
  • 不匹配时,优先怀疑单位被OCR吃掉/替换,而非合同本身矛盾
  • 模版比对报告中若涉及"费率标准不同"的结论,必须先过此验证

连锁影响

此类误读会导致模版比对报告产出错误结论(如"物业费0.4元/月与租金12.167元/月不同"),进而误导汇总表K列风险判断。修正路径:OCR产出后、写比对报告前,所有涉及金额/费率的字段过一遍数学交叉验证。

⑨ OCR严重乱码段落对模版比对的污染(2026-07-01 凤凰文化8.5-8.8实证)

问题场景

扫描件第6页8.5-8.8区域OCR产出如下乱码:

8.5 甲方按照部 门  标准 及水、电  指  和
定为  :水    国家 电价  计收
8.6 Fak    ) 租赁 期限保持 一致

基于此写出的模版比对报告将8.5概括为"水电费按部门标准及国家电价计收"(丢失具体费率),将8.6描述为"网络、电话、防火门等配套设施约定"(完全搞错——8.6只是说物业期限=租赁期限,网络电话防火门内容在8.7-8.8)。

真实内容(vision核实)

  • 8.5:水费按4.2元/吨计收;电费按国家电价+0.53元/度服务费(随国家调价联动)
  • 8.6:物业服务期限与租赁期限一致,随解除/终止而终止(仅此一句)
  • 8.7-8.8:消防远程联网、疏散楼梯、二消改造等详细消防义务

铁律

  • 模版比对报告中,每一条差异的描述必须基于可读的OCR文本或vision核实结果
  • 如果某条款区域OCR输出含3+个连续乱码词(如"Fak""peMet iy ecsapae"),该区域必须先vision确认真实内容,再写入比对报告
  • 不得对乱码文本做"合理推测"后写入比对报告——推测错误比留空更有害
  • 比对报告中遇到OCR不可读段落,标注"[OCR不可读,需原件核实]"比写错误描述强

⑩ 重做校区时 delegate_task 必须携带 vision 修正值(2026-07-01 凤凰文化实证)

问题场景

发现OCR质量问题后重做某校区,重新发 delegate_task 让 subagent 做模版比对。如果 context 只给 OCR 文件路径,subagent 读到的仍是乱码文本,会重复产出错误结论(如"物业费0.4元/月"、"8.6为网络电话条款")。

正解:在 delegate_task context 中显式列出所有 vision 修正

delegate_task(
    goal="对比租赁合同与07模版,生成逐条差异清单...",
    context="""
    文件路径:
    - 07模版: /tmp/.../07模版-全文.txt
    - 租赁合同OCR: /tmp/.../租赁合同-OCR.md
    
    关键修正信息(vision逐页核实,OCR中这些地方有乱码需用修正版):
    - 8.4条: 物业费为0.4元/㎡/天(不是/月),换算=12.167元/㎡/月
    - 8.5条: 水费4.2元/吨;电费=国家电价+0.53元/度服务费
    - 8.6条: 仅"物业期限=租赁期限"一句,不涉及网络电话
    - ...
    """,
    toolsets=["file", "terminal"]
)

铁律

  • OCR修正后重做比对 ≠ 只重发 delegate_task——必须把修正值写进 context
  • subagent 不会自动跑 vision,它只能读文件;如果文件本身有乱码而 context 没给修正,subagent 就会按乱码理解
  • 修正信息的格式:"条款号: 真实内容(不是XXX)"——明确标注哪里被误读、正确值是什么
  • 这同样适用于首次做比对时发现OCR乱码区域——先vision核实,再发subagent,context带修正值

⑪ OCR产出md文件事后核实流程(2026-07-13 桃坞路实证)

场景

md文件已产出并保存,但用户(或后续任务)质疑准确度,需要系统性核实而非重跑OCR。

核实方法:vision逐页比对

# 1. 把PDF关键页渲染成图片(200dpi够用,关键是覆盖含数据的页面)
pdftoppm -png -r 200 -f 1 -l 1 合同.pdf verify/p1
pdftoppm -png -r 200 -f 2 -l 2 合同.pdf verify/p2
# 2. vision_analyze 每张图,问具体数据点(金额、面积、日期、当事人)
# 3. 与md文件对应段落逐项比对

桃坞路4份合同核实发现的典型OCR失真模式

失真类型 频率 影响 处置
封面公章区乱码(前10-16行) 每份必现 不影响条款提取 可忽略
大写金额全乱 物业合同必现 中等——引用时必须用小写数字 以阿拉伯数字为准(已有①)
收款账户段排版错乱 偶发 低——信息可拼凑 核对账号12位数字即可
个别汉字形近误识(崇→喧、饰→4) 偶发 低——不影响数据 已知正确值时直接修正
手写填空处空白被OCR虚构文字 偶发 中——会产生虚假数据 vision确认原件是否真的填写了

核实策略(效率优先)

  1. 不需逐字全文核对——重点核对:金额、面积、日期、费率、百分比、当事人名称、账号
  2. 优先核对第2页(通常含租金/期限/保证金等核心条款),封面页和签字页优先级最低
  3. 数学交叉验证仍是最快手段:如首期=年租金÷2?物业年费=单价×面积×365?通过=可信
  4. 只需vision核对md中有乱码嫌疑的段落——正常可读的中文条款OCR准确率>98%无需逐字核
  5. 结论模板:核实后向用户报告"核心数据准确+已发现的具体问题列表",让用户知道哪些可信哪些需注意

与全流程的关系

  • 新做校区:OCR后立即跑 ocr-integrity-check.py(自动检测乱码)→ 高危区vision核实 → 写入md
  • 事后核实(本场景):直接 pdftoppm + vision 比对关键页,快速出结论
  • 两者互补:integrity-check 抓格式异常,vision比对抓语义错误(如空白处虚构文字)

与既有规则的关系

  • 这是「第一铁律·逐字通读」「OCR 不可信回原件核」在扫描件取文本环节的落地配方。
  • 费率符号(‰ vs %)的双跑/裁图核对走 references/ocr-rate-symbol-verification.md;本文管的是整篇取文本 + 抬头/金额两类高频失真 + 二值化救援 + 信用代码修复 + 签章乱码处理 + 单位误读 + 乱码段落处理