OCR:parse_layout 上传文件名写死 image.jpg,PDF 被当图片解析,且不返回文字内容(CHSAnalyzer3 文献全文) #34

Closed
opened 2026-10-22 09:19:07 +08:00 by iomgaa · 1 comment
Owner

背景

版本:polygateway 1.3.0(CHSAnalyzer3 的 chsdev 环境);仓库 HEAD 25a90eb 上 transports/monkey_ocr.py 的相关代码与之相同。

CHSAnalyzer3 要把几百篇医学论文的 PDF 交给自建的 MonkeyOCR(FastAPI),整份转成带页码的正文和表格,供后面的 LLM 抽取用。MonkeyOCR 的 POST /parse 本身支持整份 PDF(OpenAPI 描述为「Parse complete document (PDF or image)」),但经 PolyGateway 走不通。原因有两处。

问题一:上传文件名写死成 image.jpg,PDF 被当成图片解析

transports/monkey_ocr.py:208-210:

def _files(image: bytes) -> dict[str, tuple[str, bytes, str]]:
    return {"file": ("image.jpg", image, "image/jpeg")}

MonkeyOCR 是按上传文件名判断类型的,不是按内容。2026-10-21 实测,把同一份 2 页的 PDF(公开论文 PRISMA 2020 的前两页)用 curl 直连 /parse 发两次,只改 multipart 的 filename:

  • filename=doc.pdf:返回 "message":"PDF parsing (standard) completed successfully",用时 8 秒。结果包里有 doc_middle.json(pdf_info 共 2 页)、doc_content_list.json 和 doc.md。
  • filename=image.jpg:返回 "message":"image parsing (standard) completed successfully",走的是图片分支。

所以 OcrClient.parse_layout(pdf_bytes) 会被服务端按图片处理。

问题二:parse_layout 只返回版面框,不返回文字

OcrLayoutElement(types.py:1187-1196)只有 type、bbox、page_index 三个字段。_parse_pages / _parse_block 只从 _middle.json 里取块的类型和坐标。块里的文字、表格内容都没有交回调用方。

医院那条线用它定位表格区域,所以够用;但整篇文档转文字需要的正是内容本身。结果包里其实已经有现成的内容:_content_list.json 是逐块的内容,带 page_idx;.md 是整篇 Markdown,表格为 HTML 或 Markdown 形式。

希望的能力

一个「整份文档解析」的调用,比如 OcrClient.parse_document(data: bytes, *, filename: str 或 media_type: str, ...)。治理链路照旧:限流、熔断、重试、遥测、tenant_id 和 meta。

  1. 上传时按调用方给的类型设置 multipart 的 filename 和 Content-Type,PDF 用 xxx.pdf 和 application/pdf。
  2. 返回逐块的结果,每块包含:
    • 类型(title / text / table / image / equation 等,开放字符串,和现在一样不枚举锁死);
    • 页码;
    • bbox;
    • 文字内容(表格给原始的 HTML 或 Markdown)。
    • 顺序和服务端的阅读顺序一致。
  3. 最好同时交回整篇 Markdown 原文,和结果包里的 .md 一致。
  4. 页数多时单次调用耗时会远超单张图片:2 页 8 秒,论文通常 8 到 15 页。call_deadline_s 已经可以按调用覆盖,只是需要确认默认值和熔断判定不会把长文档误判成服务慢。

parse_layout 现有的行为不用动,医院那条线还在用。

使用规模

CHSAnalyzer3 的文献特征库:几百篇 PDF,估计几千页。并发不超过 2,因为这台 OCR 服务医院那条线也在用。

复现

curl -s --noproxy '*' -F "file=@two.pdf;filename=doc.pdf"   "$CHS_OCR_BASE_URL/parse"
curl -s --noproxy '*' -F "file=@two.pdf;filename=image.jpg" "$CHS_OCR_BASE_URL/parse"

两次返回的 message 不同,分别是 PDF parsing 和 image parsing。

## 背景 版本:polygateway 1.3.0(CHSAnalyzer3 的 chsdev 环境);仓库 HEAD 25a90eb 上 `transports/monkey_ocr.py` 的相关代码与之相同。 CHSAnalyzer3 要把几百篇医学论文的 PDF 交给自建的 MonkeyOCR(FastAPI),整份转成带页码的正文和表格,供后面的 LLM 抽取用。MonkeyOCR 的 `POST /parse` 本身支持整份 PDF(OpenAPI 描述为「Parse complete document (PDF or image)」),但经 PolyGateway 走不通。原因有两处。 ## 问题一:上传文件名写死成 image.jpg,PDF 被当成图片解析 `transports/monkey_ocr.py:208-210`: ```python def _files(image: bytes) -> dict[str, tuple[str, bytes, str]]: return {"file": ("image.jpg", image, "image/jpeg")} ``` MonkeyOCR 是按上传文件名判断类型的,不是按内容。2026-10-21 实测,把同一份 2 页的 PDF(公开论文 PRISMA 2020 的前两页)用 curl 直连 `/parse` 发两次,只改 multipart 的 filename: - `filename=doc.pdf`:返回 `"message":"PDF parsing (standard) completed successfully"`,用时 8 秒。结果包里有 `doc_middle.json`(`pdf_info` 共 2 页)、`doc_content_list.json` 和 `doc.md`。 - `filename=image.jpg`:返回 `"message":"image parsing (standard) completed successfully"`,走的是图片分支。 所以 `OcrClient.parse_layout(pdf_bytes)` 会被服务端按图片处理。 ## 问题二:parse_layout 只返回版面框,不返回文字 `OcrLayoutElement`(`types.py:1187-1196`)只有 `type`、`bbox`、`page_index` 三个字段。`_parse_pages` / `_parse_block` 只从 `_middle.json` 里取块的类型和坐标。块里的文字、表格内容都没有交回调用方。 医院那条线用它定位表格区域,所以够用;但整篇文档转文字需要的正是内容本身。结果包里其实已经有现成的内容:`_content_list.json` 是逐块的内容,带 `page_idx`;`.md` 是整篇 Markdown,表格为 HTML 或 Markdown 形式。 ## 希望的能力 一个「整份文档解析」的调用,比如 `OcrClient.parse_document(data: bytes, *, filename: str 或 media_type: str, ...)`。治理链路照旧:限流、熔断、重试、遥测、`tenant_id` 和 `meta`。 1. 上传时按调用方给的类型设置 multipart 的 filename 和 Content-Type,PDF 用 `xxx.pdf` 和 `application/pdf`。 2. 返回逐块的结果,每块包含: - 类型(title / text / table / image / equation 等,开放字符串,和现在一样不枚举锁死); - 页码; - bbox; - 文字内容(表格给原始的 HTML 或 Markdown)。 - 顺序和服务端的阅读顺序一致。 3. 最好同时交回整篇 Markdown 原文,和结果包里的 `.md` 一致。 4. 页数多时单次调用耗时会远超单张图片:2 页 8 秒,论文通常 8 到 15 页。`call_deadline_s` 已经可以按调用覆盖,只是需要确认默认值和熔断判定不会把长文档误判成服务慢。 `parse_layout` 现有的行为不用动,医院那条线还在用。 ## 使用规模 CHSAnalyzer3 的文献特征库:几百篇 PDF,估计几千页。并发不超过 2,因为这台 OCR 服务医院那条线也在用。 ## 复现 ```bash curl -s --noproxy '*' -F "file=@two.pdf;filename=doc.pdf" "$CHS_OCR_BASE_URL/parse" curl -s --noproxy '*' -F "file=@two.pdf;filename=image.jpg" "$CHS_OCR_BASE_URL/parse" ``` 两次返回的 `message` 不同,分别是 PDF parsing 和 image parsing。
Author
Owner

已在 1.3.11 发布(2026-10-22):v1.3.11 tag、Gitea registry 包、Release 均已就绪。

本版解决了什么

  • 新增 OcrClient.parse_document(bytes):整份 PDF/图片交给 MonkeyOCR,返回逐块的类型 / 页码 / bbox / 文字(表格为 HTML)与整篇 Markdown;治理链路(限流、熔断、重试、遥测、tenant_id/meta、call_deadline_s)与 parse_layout 同一循环。
  • 上传类型由库按内容判定(图片魔数优先,再看前 1KB 内的 %PDF-),三个方法共用。同时修复了 recognize_text(PDF) 的静默错误:此前 PDF 标成 image.jpg,服务返回 success:true 加幻觉文本。
  • 结果只读 _middle.json 与配对 .md,不读会转义 * \ ~ $的_content_list.json`。
  • 坏输入:无法识别的字节本地拒绝;服务对截断/伪造 PDF 的已知确定性 500 归「请求被拒」,不重试、不计熔断(仅新路径)。
  • 长文档超时:库内无延迟型熔断;timeout_s 不是总耗时上界,硬上限靠 call_deadline_s ≤ lease_ttl_s,README「运维须知」有部署配方。

验证

main 合并后:make lint 退出 0;全套件 2239 passed;活体 tests/integration/test_monkey_ocr_document_live.py 3 passed;真实多页 PDF 端到端得到 89 / 30 个元素(含表格 HTML)。已从 registry 重新下载 wheel,哈希与本地构建一致,并确认包内含新代码。

请注意

  • 服务端会丢弃脚注、页眉页脚、页码,库无法补回,已写入 README「已知限制」。
  • 若线上 MonkeyOCR 是同步串行后端,长文档会阻塞同实例上其它请求,请向运维确认 backend。
  • 旋转页 / 带批注页 / 加密 PDF 的差分、多行多 span 与嵌套 caption 路径无真实样本,未经实测,CHANGELOG 已登记。

后续

#35(共享分类对确定性坏输入 500)、#36(recognize_text 不校验 success)、#37(parse_layout 原生异常漏出与 download_url 主机)。

已在 **1.3.11** 发布(2026-10-22):`v1.3.11` tag、Gitea registry 包、Release 均已就绪。 ## 本版解决了什么 - 新增 `OcrClient.parse_document(bytes)`:整份 PDF/图片交给 MonkeyOCR,返回逐块的类型 / 页码 / bbox / 文字(表格为 HTML)与整篇 Markdown;治理链路(限流、熔断、重试、遥测、`tenant_id`/`meta`、`call_deadline_s`)与 `parse_layout` 同一循环。 - 上传类型由库按**内容**判定(图片魔数优先,再看前 1KB 内的 `%PDF-`),三个方法共用。同时修复了 `recognize_text(PDF)` 的静默错误:此前 PDF 标成 `image.jpg`,服务返回 `success:true` 加幻觉文本。 - 结果只读 `_middle.json` 与配对 `.md`,不读会转义 `* \` ~ $` 的 `_content_list.json`。 - 坏输入:无法识别的字节本地拒绝;服务对截断/伪造 PDF 的已知确定性 500 归「请求被拒」,不重试、不计熔断(仅新路径)。 - 长文档超时:库内无延迟型熔断;`timeout_s` 不是总耗时上界,硬上限靠 `call_deadline_s ≤ lease_ttl_s`,README「运维须知」有部署配方。 ## 验证 main 合并后:`make lint` 退出 0;全套件 2239 passed;活体 `tests/integration/test_monkey_ocr_document_live.py` 3 passed;真实多页 PDF 端到端得到 89 / 30 个元素(含表格 HTML)。已从 registry 重新下载 wheel,哈希与本地构建一致,并确认包内含新代码。 ## 请注意 - 服务端会丢弃脚注、页眉页脚、页码,库无法补回,已写入 README「已知限制」。 - 若线上 MonkeyOCR 是同步串行后端,长文档会阻塞同实例上其它请求,请向运维确认 backend。 - 旋转页 / 带批注页 / 加密 PDF 的差分、多行多 span 与嵌套 caption 路径无真实样本,未经实测,CHANGELOG 已登记。 ## 后续 #35(共享分类对确定性坏输入 500)、#36(`recognize_text` 不校验 `success`)、#37(`parse_layout` 原生异常漏出与 `download_url` 主机)。
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyGateway#34