FilesLast commitLast update
8 days ago
8 days ago
8 days ago
8 days ago
8 days ago
10 days ago
8 days ago
10 days ago
8 days ago
8 days ago
8 days ago
8 days ago
README

演示样例

这个目录下的五张图是脚本合成的,不是真实票据。它们的作用是给回放模式 (--provider replay)提供一套图片与账本严格对齐的演示数据:JSON 里每个 区域的 bbox 就是图上那段文字的实际墨迹范围,点字段能直接跳到原图那一格。

五张图覆盖五个场景(发票、服务合同、报销单、资产负债表、购销合同),跨文档 对账要拿它们放在一起比。

重新生成

cd <仓库根目录>
python3 scripts/make_samples.py          # 发票 / 服务合同 / 报销单
python3 scripts/make_samples_extra.py    # 资产负债表 / 设备采购合同

脚本用 PIL + numpy 绘制,所有随机量(手写抖动、翻拍噪点、光照不均、旋转角) 都走固定 seed,每次执行产出完全相同的图片和 JSON。

文件 内容
samples/invoice_001.png 增值税专用发票,1240×1754
samples/contract_001.png 技术开发与运维服务合同,1240×1754
samples/expense_001.png 员工报销单,手机翻拍模拟:0.9° 旋转 + 噪点 + 亮度不均
samples/financial_statement_001.png 资产负债表,1240×1754
samples/purchase_contract_001.png 设备采购合同,1240×1754
samples/sessions/demo.json 前三张图的回放会话
samples/sessions/extra.json 后两张图的回放会话

samples/real_ocr/ 里是 MiniCPM-OCR 1.0 在这三张图上的真实输出,录制命令见 scripts/record_ocr.py。

跑出来是什么

回放模式下跑完整链路(tracecell demo 或 tracecell serve):

文档 场景 区域 字段 待人工 成本
invoice-001 增值税专用发票 invoice 18 9 4 13.10 rcu
contract-001 技术服务合同 contract 19 7 2 12.60 rcu
expense-001 员工报销单 expense 17 9 2 10.70 rcu
financial-statement-001 资产负债表 financial_statement 14 10 5 8.60 rcu
purchase-contract-001 设备采购合同 purchase_contract 15 17 3 9.00 rcu
合计 83 52 16 54.00 rcu

发票 9 个字段是 发票代码、发票号码、开票日期、购买方名称、销售方名称、 纳税人识别号、金额、税额、价税合计。报销单多出来的 3 个是明细行 (明细:差旅费 / 明细:办公用品 / 明细:业务招待费),勾稽校验要用。

每份样例埋了什么

invoice-001 增值税专用发票(场景 invoice)

r014 是「价税合计(小写)¥145,318.00」。小写金额比标签小一号而且用浅灰印。 同一个区域的两条候选给出两个数:primary 读成 ¥145,318.00(0.9930), reparse-16x 把 3 看成 8 读成 ¥145,818.00(0.0000)——纸面上没有这个数。 系统没有硬选,两条都留在账本里,区域标成待人工。

同一区域还有一条 vlm-review 候选给出正确值 ¥145,318.00(0.9930),这就是 闭环里"问一句窄问题、拿一个可判定答案"的结果。另外 r015 备注区有一条分数 只有 0.2344 的候选——两个读法只差一个"为"字,纸面上分不出高下,只能靠人工。

最后 9 个字段里 7 个直接放行,价税合计(0.645)和 销售方名称(0.649)两格 进人工队列,原因写在区域的 note 里。

contract-001 技术服务合同(场景 contract)

r009「合同金额(大写):伍拾捌万元整」里"元"字印得偏淡,首次解析把"元"读成 了"圆"(0.0001),重解析才读对(0.9930)。r010「合同金额:¥580,000.00」同样 印得偏浅,重解析把 0 看成 8 读成 ¥580,800.00(0.0000)。两条候选都留在账本 里,合同金额大写(0.645)和 合同金额(0.636)两格进人工队列。

大写和小写印的是同一个数(伍拾捌万元整 = 580,000.00),互为印证。

乙方名称是「南京海川信息技术有限公司」,发票上的销售方名称是「南京海川 软件股份有限公司」。两份文档各自都识别得没错,放在一起却对不上。

这一对在 scenarios.py 的 CROSS_RULES 里是显式配对的——合同叫"乙方"、发票 叫"销售方名称",靠字段名撞车是对不上的。跑 tracecell reconcile 会被报成主体 名称不一致。字段名同名但含义不同的(发票的"金额"和报销单的"金额")只在同场景 之间比对,否则会互比出假矛盾。

expense-001 员工报销单(场景 expense)

整张图有 0.9° 旋转、轻微噪点和亮度不均,坐标跟着旋转一起变换,bbox 仍然贴着 文字。

r011「合计金额:¥2,864.50」印得又浅又小,重解析把 6 看成 9 读成 ¥2,964.50 (0.0000)——纸面上没有这个数。而三条明细 1,480.00 + 386.50 + 1,098.00 = 2,964.50,比单据上印的合计多了一百块:单据自己的合计印错了。勾稽校验直接 报出这条 critical 级问题,两侧的坐标都带上。

r003 报销人「李思远」是手写体,重解析读成「李思源」(0.1425),字段置信度 0.640。

financial-statement-001 资产负债表(场景 financial_statement)

r009「所有者权益合计:¥14,100,000.00」印得偏浅偏淡,字号比其它行小一号, 首轮识别只拿到 0.9662 分。

勾稽关系上,资产总计 = 流动资产合计 + 非流动资产合计 这条通过(12,860,000 + 8,140,000 = 21,000,000),但 资产总计 = 负债合计 + 所有者权益合计 这条不通过: 6,800,000 + 14,100,000 = 20,900,000,差 100,000。这正是资产负债表该平没平的 情况,系统报成 critical 级问题。

另外 r009「营业收入:¥128,000.00」与当期发票的 128,600.00 差 600 元,两边 OCR 都读对了,只能靠跨文档对账发现。

purchase-contract-001 设备采购合同(场景 purchase_contract)

明细是三行表格:

智能网关设备 50 2,572.00 128,600.00
边缘网关设备 20 1,180.00 26,600.00   ← 金额转置错了,正确值 23,600.00
安装配件 100 45.00 4,500.00
金额合计 156,700.00
合同总价 177,071.00(大写:壹拾柒万柒仟零柒拾壹元整)

「边缘网关设备」的金额印成了 26,600.00,正确值是 20 × 1,180.00 = 23,600.00。 这一处被两条独立的规则抓到:

  • 明细行勾稽:26,600.00 vs 23,600.00,差 3,000
  • 明细合计:印的合计 156,700.00 是按正确行算的,而各行加起来是 159,700.00

关键在于 OCR 把印错的数读得很准(0.98 分)——它确实就印在纸上。抓住它的 是算术,不是模型。

另外合同总价还有一条跨文档矛盾:177,071.00 与同期发票的价税合计对不上。

数据结构

sessions/demo.json 的格式由 src/tracecell/providers/replay.py 定义。

bbox 是归一化坐标(0~1,左上原点),由绘制时记录的像素坐标换算而来。 category 取 text / table / figure / seal。candidates[].origin 取 primary / reparse-16x / vlm-review,candidates[].verdict 取 scored / not_scored / failed。

fields[] 里的字段名都能被 src/tracecell/extractors.py 的正则抽到,字段值是 对应区域原文里的子串——用正则抽一遍,结果与这里声明的完全一致。

samples/labelled.json 是标定用的"分数 + 人工判断"标注集,由 scripts/make_labels.py 从 sessions/demo.json 导出,判定规则是"候选文本与 区域原文一致就算对",不看分数,所以不会循环论证。改了本目录的会话数据之后按 这个顺序重新生成:

python3 scripts/make_labels.py
tracecell calibrate samples/labelled.json

回放模式里的分数是录制时写死在数据里的,不是现场算出来的。要重新算就用 --provider minicpm 接上真实的 MiniCPM-OCR / Jev / MiniCPM-V 服务。