fix: label PDF uploads by content instead of a fixed image name

The service dispatches on the upload filename extension. A PDF sent as image.jpg made /ocr/text return a success response with hallucinated text. Only PDFs are relabelled; everything else keeps the legacy image label.
This commit is contained in:
2026-10-21 23:26:49 -04:00
parent 398b16e136
commit c13c4fd28f
2 changed files with 131 additions and 4 deletions
+49 -4
View File
@@ -14,7 +14,7 @@ import math
import zipfile
from contextlib import contextmanager
from io import BytesIO
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal
import httpx
@@ -42,6 +42,50 @@ if TYPE_CHECKING:
_HEALTH_TIMEOUT_S = 5.0 # 探测常量(同 jitter 系数先例,不进配置)
_MediaKind = Literal["pdf", "image", "unknown"]
# 已知图片魔数。起始命中即判图片,且**优先于** PDF 判定: 含 XMP 元数据的图片
# 可能在前 1KB 内带 `%PDF-` 字样,不能因此被当成 PDF。
_IMAGE_MAGICS: tuple[bytes, ...] = (
b"\xff\xd8\xff", # JPEG
b"\x89PNG\r\n\x1a\n", # PNG
b"II*\x00", # TIFF 小端
b"MM\x00*", # TIFF 大端
b"BM", # BMP
b"GIF87a",
b"GIF89a",
)
_PDF_HEADER = b"%PDF-"
# Adobe 阅读器容忍头部前有垃圾字节(惯例而非 ISO 32000 正文);服务端实测可解析
# 前缀 200 字节垃圾的 PDF(#34 设计 E3)
_PDF_HEADER_WINDOW = 1024
def _sniff_media(data: bytes) -> _MediaKind:
"""按内容判定上传类型: 图片魔数 → 前 1KB 内 `%PDF-` → 未知。
服务端按**文件名扩展名**分派(`.pdf` 走 PDF 数据集,其余走图片数据集),而标签
若与内容不符会得到 HTTP 500(`is no PDF`)或——在 `/ocr/text` 上——`success:true`
的幻觉文本(#34 设计 E2/E4)。所以标签只能由库按内容定,不开放给调用方声明。
"""
if data.startswith(_IMAGE_MAGICS) or (data[:4] == b"RIFF" and data[8:12] == b"WEBP"):
return "image"
if _PDF_HEADER in data[:_PDF_HEADER_WINDOW]:
return "pdf"
return "unknown"
def _upload_part(data: bytes) -> tuple[str, bytes, str]:
"""multipart 的 (filename, content, content_type)。
只有 PDF 改标 `doc.pdf`;**其余(含未知)一律沿用历史的 `image.jpg`**——服务对
PNG/TIFF/BMP 字节标 `image.jpg` 照样按内容解码(2026-10-21 实测),任何"更精确"
的改标都只会把现在能成功的路径变成失败(webp/gif 没有对应的服务端扩展名)。
"""
if _sniff_media(data) == "pdf":
return ("doc.pdf", data, "application/pdf")
return ("image.jpg", data, "image/jpeg")
def _default_client_factory(source: SourceConfig) -> httpx.AsyncClient:
return httpx.AsyncClient(
@@ -205,9 +249,10 @@ class MonkeyOcrTransport:
return client
@staticmethod
def _files(image: bytes) -> dict[str, tuple[str, bytes, str]]:
# 固定文件名/类型(CHS invokers.py:496 原样;服务按内容处理,35 样本实测)
return {"file": ("image.jpg", image, "image/jpeg")}
def _files(data: bytes) -> dict[str, tuple[str, bytes, str]]:
# 标签按内容判定(#34): 非 PDF 沿用 CHS invokers.py:496 的 image.jpg,
# PDF 改标 doc.pdf——服务按扩展名分派,固定名对 PDF 是错的
return {"file": _upload_part(data)}
def _response_json(self, resp: httpx.Response, source: SourceConfig, operation: str) -> dict:
"""响应体 JSON 化;坏 JSON/非 dict → Transient(设计 §4: 服务侧瞬时异常)。"""