# OCR 费率符号核对配方（‰ vs %，扫描件）

**适用**：扫描件合同里逾期违约金/滞纳金/利率等**日费率**字段，OCR 文本出现 `%`/`‰`/`0.5`/`1`/`2` 等，量级可疑（年化超约100%就该警觉）。这是 10× 量级错误的高发点（千分号 ‰ 被误识成百分号 %）。

## 核心原则
1. **绝不信单次 OCR 的小符号**。扫描件 OCR 对 ‰/% 经常判错。
2. **双跑交叉**：整页 OCR vs 裁图放大重 OCR。**两次不一致 = 机器判不准已证明 → 升级人工**，不在两个机器结果里挑一个。
3. **升级人工带放大裁图**（`MEDIA:` 发 Maggie 肉眼终判那一个像素符号），不甩空问题。
4. 机器判不准时，交付物先标 `〔OCR数值，单位以PDF原件为准〕`，**不武断定值**。

## 量级常识闸门（先口算，再核符号）
- 日费率写进表前先口算年化：`每日 X × 365`。
- **年化超约 100% 就该警觉**：每日 2% = 年化 730%（荒谬）；每日千分之2(2‰) = 年化 73%（合理）。
- 逾期违约金/滞纳金日费率正常落在 **万分之几 ~ 千分之几**（年化约 18%~73%）。见到"每日1%、每日2%"先疑 ‰ 被误识成 %。
- 折年化别算错：0.5%/日 = 年化 182.5%（不是18.25%）；万分之5/日才是年化 18.25%。

## 命令级配方

### 1. 定位费率条款在第几页（扫描件 PDF 文本层通常为空，必须 OCR 切图）
切图一般已在 OCR 阶段产出（`<合同名>_imgs/p<N>.png`，约 200dpi / 1654×2340）。按章节缩小页范围（如"乙方违约责任/第九条"），对候选页跑 tesseract，命中含费率特征词（`应付未付`/`滞纳金`/`延误一日`/`每延误`）的页：

```python
import subprocess, os
def ocr(img, psm=6):
    return subprocess.run(["tesseract", img, "stdout", "-l", "chi_sim+eng", "--psm", str(psm)],
                          capture_output=True, text=True).stdout or ""
# 对 <imgs>/p{N}.png 跑，命中含"滞纳金"+"延误"+"应付未付"的页即 9.2 所在页
```

### 2. 裁出费率行、放大 3–4 倍、多 psm 重 OCR
整页全文 OCR 找到费率行的行号占比，按比例裁该纵向区段（不依赖 TSV 分词，更稳）：

```python
from PIL import Image
lines = [l for l in ocr(img).split("\n") if l.strip()]
idx = next(i for i,l in enumerate(lines) if ("应付未" in l or "付金额" in l or "加付滞纳金" in l))
im = Image.open(img); W,H = im.size
frac = idx/len(lines)
y0, y1 = max(0,int(H*(frac-0.06))), min(H,int(H*(frac+0.10)))
crop = im.crop((0,y0,W,y1)).resize((W*3,(y1-y0)*3), Image.LANCZOS)
crop.save("/tmp/_rate_crop/<条款>.png")
# 对裁图再多 psm 重 OCR：for psm in (7,6,11,13): ocr(crop_path, psm)
```

### 3. 判读
- 两次（整页 vs 裁图）符号**一致** → 采信，去掉待核标记，写进表。
- 两次**不一致**（如 `0.5%` vs `0.5‰`，或 `1%` vs `1‰`）→ 机器判不准已坐实 → 走第4步。

### 4. 升级人工（带证据）
- 把第2步裁好的放大 PNG 用 `MEDIA:/tmp/_rate_crop/<条款>.png` 发 Maggie，问"数字后这个符号是 % 还是 ‰"。
- Maggie 定值后，把交付表里该条从 `〔待核〕` 改成确定值，闭环。

## ✅ vision_analyze 已配好——符号终判的主路径（2026-06-22 人民中路实证）
vision 工具已由技术支持配好可用。**现在符号判不准时，主路径是先自己 `vision_analyze` 看裁好的整页/裁图终判**，能自核就不必裁图发 Maggie；自核仍拿不准的才交人。crop-OCR 多 psm 仍可能裁偏（本次裁两次都偏到隔壁条款），vision 看整页反而一步到位。

**🔑 vision 终判的精髓 = 让它做「同页符号交叉对比」**，不要只问"这是%还是‰"。提问里点名让它拿**同一页别处确定的符号**作参照：
> "请找到第十条4款的违约金率符号，并和同一页第2款/第3款的「10%」对比——4款那个符号右下方是一个圆圈(%)还是两个圆圈(‰)？"

实证：人民中路租赁第十条4款，vision 自动拿同页第2/3款的"10%"百分号比对，确认4款符号"明显比%更宽、右下方两个圆圈"= **0.1‰**（年化3.65%）；物业第九条2款同法确认 **0.5%**（年化182.5%）。同页对比比孤立看一个符号可靠得多——人眼/模型判 ‰vs% 都靠"和已知符号比宽窄、数圆圈个数"。

**仍要带量级闸门复核**：vision 给的符号要和年化常识对得上（0.1‰=3.65%对逾期违约金偏低但合理；若 vision 说某逾期费率是"每日5%"=年化1825%就该反问）。vision 偶发 `Connection error`，重试即可（本次租赁那张第一次 Connection error、重试成功）。

## 🔴 发 vision 前必须先压缩图片——防超时（2026-06-23 实测确立）
当前 vision 配置：`gemini-3.5-flash`（经 litellm-sora 代理），**timeout 仅 30 秒**。实测：
- **2.5MB 原图（200dpi PNG，1654×2340）→ Request timed out 失败**
- **压到 ~385KB（宽1100px JPG q85）→ 秒过、读得准**
所以**铁律：任何图发 vision_analyze 前，先 resize 到宽 ≤1100px、转 JPG，体积压到 ~300–400KB**：
```python
from PIL import Image
im = Image.open(src)
w,h = im.size; nw = 1100; nh = int(h*nw/w)
im.resize((nw,nh), Image.LANCZOS).convert("RGB").save(dst, quality=85)
```
- 压缩后清晰度仍足够 vision 数圆圈、读符号、判版面（实测 0.1‰/% 区分无误）。
- 超时是"图太大传不完"，不是模型不行——别因一次 timeout 就判 vision 不可用，先压图重试。
- 单次失败重试 1–2 次（偶发 Connection error）；连续失败才退兜底（tesseract 双跑+裁图发 Maggie）。

## ✅ vision 能力边界 —— 定位"精核兜底"，不是"批量主力"（2026-06-23 实测确立）
vision 在差质量扫描件（文字层=0 的纯扫描 PDF，正是本项目 PDF 识别出问题的根因）上**实测够用**，但用对位置：
- **✅ 适合（单页/单点精读核对）**：① 费率符号 ‰/% 同页交叉终判 ② 标红颜色是否真红（配 PIL 像素检测）③ 版面横向/纵向截断、跨页切断 ④ 关键数字（金额/日期/比例）人工复核兜底。
- **❌ 不适合（批量全文提取）**：8 页合同全文 OCR 不要逐页喂 vision——一页一次调用、还可能超时，慢且不划算。**全文底料仍用传统 OCR（deepseek-ocr / tesseract）跑一遍**。
- **最佳架构（已被实测验证 = 现行 skill 设计）**：传统 OCR 出全文底料 → vision 精核可疑点/符号/版面。两层各司其职，不互相替代。
- **交付前 vision 三查清单**（图都先压缩）：① 文字完整（pdftotext 拍平 grep）② 视觉呈现（vision 看渲染图：截断/错位/红色）③ 符号量级（vision 同页对比 + 年化闸门）。三查全过再交。

## 退路（vision 万一又不可用）
- 若 `vision_analyze` 报 `No LLM provider configured for task=vision` 或持续连不上：退到 **tesseract 双跑 + 裁图发 Maggie**（上面命令级配方）。
- 这是兜底，不是主路径——vision 已配好，优先自核。

## 像素分析绕过 vision 核「单元格字体颜色」（2026-06-22 世茂实证）

**场景**：交付的标红 xlsx 改完，要确认「某条是不是红字 / 整格有没有误染红」，但 vision 未配、自己看不了图。**不必干等 WeiWei 配 vision**——x2t 渲染成 PDF→PNG 后，用 PIL+numpy 直接读像素统计红/黑占比，机器就能给出客观判断。

```python
from PIL import Image
import numpy as np
img = Image.open("渲染页.png").convert("RGB")
arr = np.array(img)
r,g,b = arr[:,:,0].astype(int), arr[:,:,1].astype(int), arr[:,:,2].astype(int)
red_mask   = (r>120)&(g<90)&(b<90)&(r-g>50)&(r-b>50)   # 明显红字
black_mask = (r<90)&(g<90)&(b<90)                       # 黑字
# 逐 20px 水平带判主色，能定位「哪几行是红的」，比整页占比更准
for y in range(0, arr.shape[0], 20):
    br, bk = red_mask[y:y+20].sum(), black_mask[y:y+20].sum()
    if br+bk < 100: continue          # 跳过空白带
    print(y, "红" if br>bk else "黑")
```

- **判读**：红字带 ≈ 该红的条数 → 正常；红字带远多于黑字带 → 整格误染红，要查 XML。
- ⚠️ **但像素只是辅助**：渲染会让部分红 rPr 不生效、红黑混杂，像素比例不绝对。**XML 层的精确计数才是真相源**——核颜色最终回 `sharedStrings.xml` 数 `rgb="FFFF0000"`（见下条 bug 教训）。像素分析用于「快速判断有没有大面积异常」，精确定位用 XML。

## ⚠️ 颜色计数 bug：精确匹配 `rgb="FFFF0000"`，绝不子串匹配（2026-06-22 教训）

数红色 run 时**必须精确正则** `re.findall(r'rgb="FFFF0000"', xml)`，**绝不能** `'FF0000' in etree.tostring(rpr)`——黑色 `FF000000` 里也含子串 `F0000`，子串匹配会把每个黑字 run 误判成红字，导致长单元格（多 run 的 K列风险格）被误报「整格泛红」。世茂栽点：`red_run_count` 一度报 K8/K11/A16 各 8/15/20 个红 run、全表 47 红，虚惊一场要返工；精确匹配后真红就 6 处全对（4个付款提示 H列 + K8第1条 + A16第6条「需核实」句）。`edit-redmarked-xlsx.py` 的 `red_run_count` 已修为精确匹配。**任何「整格泛红」的判断，先用精确 `rgb="FFFF0000"` 复核再下结论。**

## 实证（世茂校区，2026-06-18）
| 条款 | 整页OCR | 裁图重OCR | 判定 |
|---|---|---|---|
| 租赁14.1 逾期付款违约金 | `2%` | （上午已核）`千分之2` | ✅ 确认 **2‰**（年化73%合理；2%=年化730%荒谬） |
| 青少物业9.2 滞纳金 | `0.5%`(读成"0.5吃") | `0.5‰` | ⚠️ 打架 → 裁图发 Maggie 终判 |
| 高中物业9.2 滞纳金 | `1%` / `1‰`（两跑不同） | `1‰` | ⚠️ 打架 → 裁图发 Maggie 终判 |

教训：四份合同同批扫描，OCR 在 ‰/% 上三跑三种组合。任何一条都不能拿单次 OCR 定稿。
