9.8 KiB
OCR 费率符号核对配方(‰ vs %,扫描件)
适用:扫描件合同里逾期违约金/滞纳金/利率等日费率字段,OCR 文本出现 %/‰/0.5/1/2 等,量级可疑(年化超约100%就该警觉)。这是 10× 量级错误的高发点(千分号 ‰ 被误识成百分号 %)。
核心原则
- 绝不信单次 OCR 的小符号。扫描件 OCR 对 ‰/% 经常判错。
- 双跑交叉:整页 OCR vs 裁图放大重 OCR。两次不一致 = 机器判不准已证明 → 升级人工,不在两个机器结果里挑一个。
- 升级人工带放大裁图(
MEDIA:发 Maggie 肉眼终判那一个像素符号),不甩空问题。 - 机器判不准时,交付物先标
〔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,命中含费率特征词(应付未付/滞纳金/延误一日/每延误)的页:
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 分词,更稳):
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%vs0.5‰,或1%vs1‰)→ 机器判不准已坐实 → 走第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:
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 直接读像素统计红/黑占比,机器就能给出客观判断。
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 定稿。