diff --git a/src/polygateway/types.py b/src/polygateway/types.py index a9ffcc9..4d83b16 100644 --- a/src/polygateway/types.py +++ b/src/polygateway/types.py @@ -201,6 +201,68 @@ class GlobalLimits: raise ValueError("全局限额不能为负(0 表示不启用)") +@dataclass(frozen=True) +class OcrLayoutElement: + """版面单元(M3 设计 §3.1): 来自 `_middle.json` para_blocks 的带类型块。 + + type 为开放字符串(实测 table/image/text,不枚举锁死——零业务假设); + bbox 为 OCR 原生页面坐标 (x1, y1, x2, y2),几何映射留业务侧(D9)。 + """ + + type: str + bbox: tuple[float, float, float, float] + page_index: int + + +@dataclass(frozen=True) +class OcrTextResult: + """一次治理 OCR 文本转录的统一响应(/ocr/text;M3 设计 §3.1)。 + + text 空串 = 合法"无文字";行过滤/去重/拼帧留业务侧(VT 迁移 §3)。 + """ + + text: str + source_name: str + usage: Usage # OCR 无计费: Usage(0, 0);耗时由 latency_ms 承载 + latency_ms: int + call_id: str + raw: dict[str, Any] + + +@dataclass(frozen=True) +class OcrLayoutResult: + """一次治理版面解析的统一响应(/parse → ZIP;M3 设计 §3.1)。 + + elements 空 = 合法"无元素";CHS 首表 = 首个 type=="table" 元素。 + page_sizes 按 page_index 索引。 + """ + + elements: list[OcrLayoutElement] + page_sizes: list[tuple[float, float]] + source_name: str + usage: Usage + latency_ms: int + call_id: str + raw: dict[str, Any] + + +@dataclass(frozen=True) +class OcrTextTransportResult: + """transport 单次 /ocr/text 调用产物;治理字段由 OcrClient 补齐。""" + + text: str + raw: dict[str, Any] + + +@dataclass(frozen=True) +class OcrLayoutTransportResult: + """transport 单次 /parse 两段调用产物;治理字段由 OcrClient 补齐。""" + + elements: list[OcrLayoutElement] + page_sizes: list[tuple[float, float]] + raw: dict[str, Any] + + @dataclass(frozen=True) class EmbeddingTransportResult: """一次原始 embedding 调用的解析结果(M2 设计 §7.2;transport → client)。""" diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index de44e03..d211b53 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -154,3 +154,61 @@ class TestAuxTypes: raw={"id": "x"}, ) assert s.raw["id"] == "x" + + +class TestOcrTypes: + """M3 OCR 五类型(设计 §3.1): 可构造、frozen、全必填无默认。""" + + def _element(self): + from polygateway.types import OcrLayoutElement + + return OcrLayoutElement(type="table", bbox=(41.0, 48.0, 218.0, 282.0), page_index=0) + + def test_layout_element_fields(self): + el = self._element() + assert el.type == "table" and el.bbox == (41.0, 48.0, 218.0, 282.0) and el.page_index == 0 + with pytest.raises(dataclasses.FrozenInstanceError): + el.type = "text" + + def test_text_result_provenance(self): + from polygateway.types import OcrTextResult + + r = OcrTextResult( + text="TOSHIBA", + source_name="monkey_1", + usage=Usage(0, 0), + latency_ms=3200, + call_id="cid", + raw={"success": True}, + ) + assert r.text == "TOSHIBA" and r.usage.prompt_tokens == 0 + with pytest.raises(dataclasses.FrozenInstanceError): + r.text = "x" + + def test_layout_result_provenance(self): + from polygateway.types import OcrLayoutResult + + r = OcrLayoutResult( + elements=[self._element()], + page_sizes=[(759.0, 540.0)], + source_name="monkey_1", + usage=Usage(0, 0), + latency_ms=8000, + call_id="cid", + raw={"has_table": True}, + ) + assert r.elements[0].type == "table" and r.page_sizes[0] == (759.0, 540.0) + + def test_transport_results(self): + from polygateway.types import OcrLayoutTransportResult, OcrTextTransportResult + + t = OcrTextTransportResult(text="", raw={}) + assert t.text == "" # 空串 = 合法"无文字" + lt = OcrLayoutTransportResult(elements=[], page_sizes=[(1.0, 1.0)], raw={}) + assert lt.elements == [] # 空 elements = 合法"无元素" + + def test_all_fields_required(self): + from polygateway.types import OcrTextResult + + with pytest.raises(TypeError): + OcrTextResult(text="x") # 溯源件不可省略