把 OvisOCR2 接进现有文档流水线,比较稳妥的顺序是:先拿一页真实业务文档跑通输入和输出,再把返回结果转换成自己的中间结构,然后用百页以内的小批量把失败页暴露出来,接着确认重复调用是否稳定,最后才决定放量。跳过这些步骤的代价通常不是模型不准,而是接口契约没摸清、下游解析跟着返回格式变、批量跑到一半才发现某类页面全都处理失败。
建议按「单页摸形态 → 统一中间结构 → 百页内小批量 → 重跑一致性 → 再定放量」五步走,每一步都留下可核验的证据:原始返回、落盘样例、失败页日志、重复调用比对结果。接口名、参数名、坐标基准未确认前一律用占位符,不要先写解析逻辑再补验证,否则换版本或换模型时下游都要返工。
先用单页确认输入输出形态
挑选一张最有代表性的业务页,最好同时包含表格、双栏、页眉页脚,并带一点扫描痕迹;先用它跑一次最小调用,再补一次异常场景(比如传一个不存在的路径)。两次返回都要原样存下来,作为后续解析和重试逻辑的参照。
下面是一个通用调用骨架,接口名、参数名、返回字段都是占位,需要按实际 SDK 或服务文档替换;未确认的地方不要猜。
# 通用骨架:接口名/参数名均为占位,需按实际 SDK 确认
client = OCRClient(endpoint=ENDPOINT, api_key=API_KEY) # 构造方式待确认
resp = client.recognize(
input=PAGE_PATH, # 待确认:整页图像路径 / 文件对象 / URL
page_range='1', # 待确认:是否支持多页文件与页码范围
options={'lang': 'zh', # 待确认:语言、方向、版面开关的真实参数名
'return_layout': True}
)
# 先打印原始结构,不要急着取字段
print(type(resp), dir(resp))
print(json.dumps(resp, ensure_ascii=False)[:2000])
如果服务提供命令行入口,也可以先用一条命令把请求和返回写到文件里,避免被 SDK 封装挡住视野:
# 示意:实际参数以服务文档为准
ovisocr2-cli recognize `--input` page_001.png `--out` raw/page_001.json `--verbose`
head -c 800 raw/page_001.json
单页验证要确认的具体项,建议逐条打勾:
- 输入接受的是整页图像还是文件(PDF、多页 TIFF),多页文件是一次返回全部页还是只返回首页;
- 返回顶层是纯文本字符串、块数组,还是带 pages/blocks/lines 的层级结构;
- 块是否带类型字段(正文、标题、表格、图注),是否带阅读顺序;
- 坐标是像素还是归一化,原点在左上还是左下,基于原图还是缩放后的图;
- 失败时返回什么:异常、错误码,还是 200 但结果为空。空结果和识别失败必须能区分,否则无法判断是否该重试。
这一步没跑通就不要往下走,因为后面所有判断都建立在输出形态已经固定的前提上。
把输出落成统一中间结构
下游流程不应该直接读取模型返回。建议在调用层和业务层之间加一层转换,把每次识别结果写成固定的中间结构,字段先定四类:页码、块类型、文本、坐标,再补阅读顺序和原始引用。
字段定义可以先按下面这一组固定下来:
- page_no:从 1 开始的页码,多页文件按页展开成多条记录;
- block_type:正文、标题、表格、图注等,未知类型统一记为 other,不要丢;
- text:该块的文本;表格可以存结构化行,也可以存原始文本,但字段上要标明是哪种;
- bbox:[x0, y0, x1, y1],统一换算到原图像素坐标,并在 schema 注释里写明基准;
- order:阅读顺序,缺失时按块在返回中的下标填充;
- raw_ref:原始返回文件路径或 request_id,便于回溯;
- engine_version:模型或接口版本标识,换版本时能按页区分。
落盘用 JSON Lines 比较省事,一行一个块,后续按页过滤和 diff 都方便:
{"page_no": 1, "block_type": "title", "text": "季度经营情况说明", "bbox": [72, 60, 520, 96], "order": 0, "raw_ref": "raw/doc001_p1.json", "engine_version": "ovisocr2-[ver]"}
{"page_no": 1, "block_type": "text", "text": "本季度……", "bbox": [72, 120, 540, 210], "order": 1, "raw_ref": "raw/doc001_p1.json", "engine_version": "ovisocr2-[ver]"}
{"page_no": 2, "block_type": "table", "text": "行1列1|行1列2", "bbox": [70, 88, 545, 300], "order": 0, "raw_ref": "raw/doc001_p2.json", "engine_version": "ovisocr2-[ver]"}
落盘之后再补一个 schema 校验:字段是否齐全、bbox 是否为四个数、坐标是否落在图幅范围内。不合格的页直接标记为待人工处理,比让不合格数据流到下游更容易收拾。这一段的验证方式很直接:拿同一页的单页原始返回和落盘结果对一遍,块数、文本、坐标能对上即可。
小批量跑并统计失败页
百页以内批跑的目的不是看整体效果,而是把失败集中暴露出来。样本要刻意混进几类页:扫描件、手机拍照、带表格的、超长页、空白页、加密或损坏文件。批跑脚本建议带并发上限、单页超时、失败重试和逐页日志。
# 百页以内批跑骨架:参数为占位,需按实际接口替换
import json, time
def run_batch(pages, max_retry=2):
with open('logs/batch.jsonl', 'a', encoding='utf-8') as log:
for page in pages: # 串行示意,并发需另加限流
for attempt in range(1, max_retry + 2):
t0 = time.time()
try:
resp = recognize(page) # 调用层封装,内部设超时
save_blocks(page, resp) # 转中间结构并落盘
log.write(json.dumps({'page': page.name, 'attempt': attempt,
'status': 'ok',
'ms': int((time.time() - t0) * 1000)},
ensure_ascii=False) + '\n')
break
except Exception as e:
log.write(json.dumps({'page': page.name, 'attempt': attempt,
'status': 'fail',
'err_type': type(e).__name__,
'err_msg': str(e)[:200],
'ms': int((time.time() - t0) * 1000)},
ensure_ascii=False) + '\n')
if attempt > max_retry:
mark_for_manual(page) # 进人工队列,不要静默丢弃
else:
time.sleep(2 * attempt) # 退避间隔按接口限流要求调整
重试只对可恢复错误有意义:超时、连接中断、限流可以重试;文件损坏、格式不支持、权限失败,重试几次都是同一个结果,直接进人工队列更省时间。日志里必须同时有页码和错误原因,否则事后只能看到「有一批失败了」,定位不到是哪类页面、卡在哪个环节。
跑完做一份简单的失败分布:按错误类型数一数失败页数量、列出全部失败页文件名、看失败是否集中在某个来源或某种版式。这里不需要精确指标,能回答「失败是不是有规律」就够了。
确认重复调用的结果一致性
重跑是否安全,取决于同一页多次识别的输出差异有多大。建议从小批量里挑十页左右,正常页和之前失败过的页都要有,每页跑三到五次,逐次落盘再比对。
# 一致性比对骨架
for page in sample_pages:
runs = [normalize(recognize(page)) for _ in range(5)]
base = runs[0]
for i, r in enumerate(runs[1:], start=1):
print(page.name, i,
'块数', len(base), len(r),
'文本一致', [b['text'] for b in base] == [b['text'] for b in r],
'坐标最大偏移', max_bbox_delta(base, r))
比对前先把天然不稳定的字段排除掉:request_id、时间戳、耗时、临时下载链接、置信度的小数位。剩下要比的是块数量、块顺序、文本内容、坐标偏移。判断标准可以先这样定:如果只是坐标有像素级抖动、文本完全一致,按幂等处理即可;如果文本或块数会变,就要在下游补去重规则或固定版本策略,不能假设重跑无副作用。
把不稳定字段单独记成一张表,写明字段名、变化范围、是否影响下游。这张表以后换模型版本时还能继续用,比每次重新试一遍省事。
再决定是否全量接入
放量条件建议在跑批量之前就写下来,避免事后找理由。可以用两条必须满足的标准:第一,失败页占比要落在团队当天能人工消化的量之内,阈值可以根据你们人工复核的容量反推出来,写进配置而不是口头约定;第二,抽检要能通过,从批跑结果里按来源随机抽若干页,人工逐页核对文本和块切分,错漏在可接受范围内才放行。两条标准都建议留余量,宁可先按来源放一部分。
放量节奏可以按来源、按文档类型分批开始,而不是一次全部切换。回滚方式要提前准备,并实际演练一次:
- 开关:调用层保留按来源的启用标记,关掉即退回原有人工流程或旧流程;
- 旁路:识别结果先写中间表,不进正式库,人工确认后再合并,出问题时只需丢弃中间表;
- 幂等键:用文件哈希加页码做键,重跑不会重复生成记录,回滚后重放也不会留下异常数据;
- 人工队列:失败页和低置信页统一进复核队列,队列深度作为是否继续放量的观察项。
这四条都到位,再讨论全量;任何一条只能口头保证,就先停在小批量阶段。OvisOCR2 接进文档流水线,真正花时间的往往不是调用本身,而是中间结构、失败处置和回滚路径,这三样在单页阶段就该开始写。