# 纯扫描件合同 OCR 配方（image-only PDF → 可读文本）

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

## 配方（tesseract 引擎，中英混排）

```bash
# 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 可读，但租金表里的**中文大写金额**（"壹拾壹万玖仟…"）常被识别成乱码（`SRSAU`、`SRSAUT SAAR` 之类）。处置：
- **关键金额一律以阿拉伯数字栏为准**（如 `119,190.75`），大写乱码忽略，**不照抄乱码进表**。
- 数额存疑/阿拉伯数字也不清 → 回原图 `vision_analyze` 核，或走金额数学交叉验证（不含税×(1+税率)=含税，见 SKILL「4d/数学交叉验证」）。

### ② 抬头/公司名乱码 → 裁高清局部图 vision 逐笔辨认 + 多处交叉核
扫描件首页抬头（甲方/乙方公司全称）常因字号小/印章压字而 OCR 乱码，**且这是梳理表的关键基础项，不能蒙**。处置：
```bash
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
```python
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')
```
```bash
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 文字层检测
```python
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% 文本）**
```bash
# 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 仍乱码的具体字符）**
```python
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 把统一社会信用代码中的英文字母部分读乱（如 `MA1NAEBX26` → `MA INAEBX26`，数字间插入空格；`MADQRFT66P` 被完整读出但无法确认是否正确）。`ocr-integrity-check.py` 将 3+ 连续大写字母匹配为"公司名乱码"——但信用代码中的字母部分是合法的。

### ocr-integrity-check.py 修复（已落地 2026-06-29）
脚本增加信用代码排除逻辑：检测匹配到的字母序列前后是否有数字，有则判定为信用代码的一部分并跳过。

### 网络交叉验证技巧
OCR 信用代码不完整时，用企查查/天眼查 web search 验证：
```
web_search: "公司全称" 统一社会信用代码
```
已验证案例：
- 南通青创企业管理咨询有限公司：91320600MA1NAEBX26（企查查）
- 南通新东方教育科技有限公司：91320602MADQRFT66P（校验位验证通过）
- 南通业玖物业管理有限责任公司：企查查未查到（可能是小公司或名称细微差异），但 tesseract 重跑清晰读出

### 信用代码校验位验证
```python
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 产出大量无意义英文字母序列（`AAA`、`LIV`、`BREE`、`RUE`、`ANON` 等），是印章/手写签名/骑缝章被 OCR 引擎误识别的产物。

### 处理方式
```python
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 核实
```python
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"）。类似地，"月""年"等单位后缀也可能被吃掉。

### 检测方法：数学交叉验证
合同中单价通常后面紧跟一个**月总额表格**。验证公式：
```python
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 修正
```python
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逐页比对
```bash
# 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`；本文管的是**整篇取文本 + 抬头/金额两类高频失真 + 二值化救援 + 信用代码修复 + 签章乱码处理 + 单位误读 + 乱码段落处理**。
