从扫描件里取出人会读的那个顺序的文本
朴素 OCR 按检测顺序返回页面上的所有词,而那并不是任何人阅读的顺序。这是一份关于还原阅读顺序的纯文本抽取指南——附带逐区块坐标,以及“文字有没有被改写”的校验。
“把文字给我就行” —— 听上去是最简单的诉求,却是多数 OCR 集成悄悄做错的一环。
视觉 OCR 引擎会把找到的词按段落归组,以 检测顺序 返回:大致自上而下、自左而右扫过整页。单栏便签恰好与阅读顺序一致;双栏文章、带侧栏的发票、略有倾斜的扫描件就不是了。你会拿到左栏第一段、然后右栏第一段、再回到左栏。词都没错,文档却读不通。
如果这些文字进入检索索引、向量或差分比对,损害是安静的:不报错,只是结果慢慢变差,而没人会追溯到 OCR 这一步。
即使不保留结构,纯文本仍需要三件事
就算没有结构要保留,可用的纯文本抽取也必须回答三个问题:
- 什么顺序? 区块要按阅读顺序出来,被换行截断的句子要重新接上,而不是留成两截。
- 来自哪里? 如果你索引了一个段落,之后需要向人解释这份文档为什么命中,你需要那个段落的坐标——页码不够。
- 是不是页面上写的? 转写页面的模型也会顺手“规范化”。悄悄的改良比错误更糟,因为它读起来是对的。
一次调用
POST /ocr/text 接收一张图像并返回文档文本。加上 includeBlocks,还会拿到每个区块及其坐标。
curl -X POST https://api.space-ocr.com/ocr/text \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/page.jpg",
"imageType": "url",
"includeBlocks": true
}'{
"status": "success",
"data": {
"text": "樱花商事株式会社\n发票\n合计 1,451",
"source": "llm",
"image_size": { "width": 1654, "height": 2339 },
"review_summary": {
"unit": "block",
"total": 12,
"boxed": 12,
"text_verified": 11,
"text_mismatch": 1,
"needs_review": 1,
"flagged": [{ "path": "blocks[7]", "reason": "text_mismatch" }],
"recovered_blocks": 0,
"token_coverage": 1.0
},
"blocks": [
{
"text": "樱花商事株式会社",
"bbox": { "xmin": 60, "ymin": 48, "xmax": 470, "ymax": 92 },
"vertices": [{ "x": 60, "y": 48 }, "…"],
"bbox_source": "token_id",
"text_verified": true,
"needs_review": false
}
]
}
}text 是整份文档拼成的一个字符串(区块以换行连接)。blocks[] 是同样的内容,按实际定位到的单位切分,每个区块带 0–1000 归一化的 bbox 与 vertices。
分工值得写清楚:字符与坐标以视觉 OCR 为准,模型只被问及顺序与归组。 由它决定下一个区块是哪个、哪些片段属于同一块——它无法凭空造出坐标,对字符也没有最终决定权。
text_verified,以及它为 false 的时候
每个区块都带 text_verified。引擎取出该区块所主张的词元,查出视觉 OCR 在这些坐标上读到的内容,归一化后比对。true 表示两次判读一致;false 表示转写与页面不同——文字照样返回,被标记出来而不是悄悄出错。
这不是准确率百分比。它回答的是更窄也更有用的问题:这条有没有和像素对过,并且对上了? 对索引流水线来说,一个自然的策略是全部入库但保留标记,等到最终要展示给人时,不确定的区块被标出来,而不是当作事实呈现。
两种失败被显式处理,而不是藏起来。 如果模型这一路彻底失败(无密钥、配额用尽、重试耗尽),端点不会报错,而是降级为视觉转写并如实告知:source: "vision" 加一条 warning。另外,任何没有被任何区块主张的页面词元区间,都会作为回收区块(bbox_source: "unclaimed_tokens")追加回来,于是丢掉的段落会出现在输出里,而不是从输出里消失。
什么时候该关掉模型
useLlm: false 给你纯视觉转写:即时、无模型成本,代价是区块按原始 OCR 顺序。对于顺序本就正确的单栏页面,或只需判断关键词是否存在的批量处理,这是划算的取舍。
当顺序承载含义时就留着它——多栏排版、带侧栏的表单、有倾斜的扫描件,以及要给人看而不只是拿去匹配的文本。
如果你要的是结构而不是行文(标题作为标题、表格作为表格),那是另一个端点:POST /ocr/markdown 会返回保留版式的 Markdown,带同样的逐元素坐标与同样的 text_verified。