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:
@@ -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: 服务侧瞬时异常)。"""
|
||||
|
||||
Reference in New Issue
Block a user