From ab6d42aecd453c08f9ea198873be820a33764cd4 Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Fri, 3 Apr 2026 13:17:26 +0200 Subject: [PATCH] Move bbox.py from domain/ to infra/ and unify coordinate logic domain/ must be pure with no external dependencies. bbox.py imports docling_core and belongs in infra/. Also refactor ServeConverter to use the canonical to_topleft_list via BoundingBox instead of duplicated manual coordinate conversion. Move docling-core to base requirements since it is now needed in both modes. --- README.md | 4 +-- docs/architecture.md | 2 +- docs/bbox-pipeline.md | 2 +- document-parser/{domain => infra}/bbox.py | 0 document-parser/infra/local_chunker.py | 2 +- document-parser/infra/local_converter.py | 2 +- document-parser/infra/serve_converter.py | 31 +++++++++++++---------- document-parser/requirements-local.txt | 1 - document-parser/requirements.txt | 1 + document-parser/tests/test_bbox.py | 2 +- 10 files changed, 25 insertions(+), 22 deletions(-) rename document-parser/{domain => infra}/bbox.py (100%) diff --git a/README.md b/README.md index fda0a1a..cee5b1f 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ document-parser/ ├── main.py # FastAPI app, CORS, lifespan ├── domain/ # Pure domain — no HTTP, no DB │ ├── models.py # Document, AnalysisJob dataclasses -│ ├── parsing.py # Docling conversion & page extraction -│ └── bbox.py # Bounding box coordinate normalization +│ ├── ports.py # Abstract protocols (converter, chunker) +│ └── value_objects.py # ConversionResult, PageDetail, ChunkResult ├── api/ # HTTP layer (FastAPI routers) │ ├── schemas.py # Pydantic DTOs (camelCase serialization) │ ├── documents.py # /api/documents endpoints diff --git a/docs/architecture.md b/docs/architecture.md index 9cfcade..f51795f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -68,7 +68,7 @@ document-parser/ | Layer | Role | Depends on | |-------|------|------------| -| **domain** | Dataclasses, bbox math, Docling conversion | Nothing (pure Python) | +| **domain** | Dataclasses, value objects, ports | Nothing (pure Python) | | **persistence** | SQLite CRUD, aiosqlite | domain (models) | | **services** | Orchestrate use cases, call Docling | domain + persistence | | **api** | HTTP endpoints, Pydantic DTOs, error handling | services | diff --git a/docs/bbox-pipeline.md b/docs/bbox-pipeline.md index b0be9e1..4f2e0fe 100644 --- a/docs/bbox-pipeline.md +++ b/docs/bbox-pipeline.md @@ -38,7 +38,7 @@ The frontend converts PDF points to CSS pixels, then the canvas renders at `devi ## Transformation 1 — `to_topleft_list()` -**File:** `document-parser/domain/bbox.py` +**File:** `document-parser/infra/bbox.py` Normalizes any Docling bbox to `[left, top, right, bottom]` in TOPLEFT coordinates. diff --git a/document-parser/domain/bbox.py b/document-parser/infra/bbox.py similarity index 100% rename from document-parser/domain/bbox.py rename to document-parser/infra/bbox.py diff --git a/document-parser/infra/local_chunker.py b/document-parser/infra/local_chunker.py index 500b691..1b59406 100644 --- a/document-parser/infra/local_chunker.py +++ b/document-parser/infra/local_chunker.py @@ -15,7 +15,7 @@ from docling_core.transforms.chunker import HierarchicalChunker from docling_core.transforms.chunker.hybrid_chunker import HybridChunker from docling_core.types.doc.document import DoclingDocument -from domain.bbox import EMPTY_BBOX, to_topleft_list +from infra.bbox import EMPTY_BBOX, to_topleft_list from domain.value_objects import ChunkBbox, ChunkingOptions, ChunkResult logger = logging.getLogger(__name__) diff --git a/document-parser/infra/local_converter.py b/document-parser/infra/local_converter.py index 9e0b23a..765f954 100644 --- a/document-parser/infra/local_converter.py +++ b/document-parser/infra/local_converter.py @@ -35,7 +35,7 @@ from docling_core.types.doc import ( TitleItem, ) -from domain.bbox import to_topleft_list +from infra.bbox import to_topleft_list from domain.value_objects import ( ConversionOptions, ConversionResult, diff --git a/document-parser/infra/serve_converter.py b/document-parser/infra/serve_converter.py index 137a0a9..a6a0502 100644 --- a/document-parser/infra/serve_converter.py +++ b/document-parser/infra/serve_converter.py @@ -18,6 +18,7 @@ import mimetypes from pathlib import Path import httpx +from docling_core.types.doc.base import BoundingBox, CoordOrigin from domain.value_objects import ( ConversionOptions, @@ -25,6 +26,7 @@ from domain.value_objects import ( PageDetail, PageElement, ) +from infra.bbox import to_topleft_list logger = logging.getLogger(__name__) @@ -222,21 +224,22 @@ def _add_element(item: dict, pages: dict[int, PageDetail]) -> None: def _extract_bbox(bbox_data: dict, page_height: float) -> list[float]: - """Extract and normalize bbox to TOPLEFT [l, t, r, b] format.""" + """Extract and normalize bbox to TOPLEFT [l, t, r, b] format. + + Delegates to the canonical to_topleft_list function via a docling-core + BoundingBox, ensuring consistent coordinate handling across all converters. + """ if not isinstance(bbox_data, dict): return [0.0, 0.0, 0.0, 0.0] - left = bbox_data.get("l", 0.0) - top = bbox_data.get("t", 0.0) - right = bbox_data.get("r", 0.0) - bottom = bbox_data.get("b", 0.0) - coord_origin = bbox_data.get("coord_origin", "TOPLEFT") + origin_str = bbox_data.get("coord_origin", "TOPLEFT") + origin = CoordOrigin.BOTTOMLEFT if origin_str == "BOTTOMLEFT" else CoordOrigin.TOPLEFT - if coord_origin == "BOTTOMLEFT": - # In BOTTOMLEFT: top has higher y, bottom has lower y - # In TOPLEFT: flip both — new_top = page_height - old_top - new_top = page_height - top - new_bottom = page_height - bottom - top, bottom = new_top, new_bottom - - return [left, top, right, bottom] + bbox = BoundingBox( + l=bbox_data.get("l", 0.0), + t=bbox_data.get("t", 0.0), + r=bbox_data.get("r", 0.0), + b=bbox_data.get("b", 0.0), + coord_origin=origin, + ) + return to_topleft_list(bbox, page_height) diff --git a/document-parser/requirements-local.txt b/document-parser/requirements-local.txt index 82cf543..fc2f3c7 100644 --- a/document-parser/requirements-local.txt +++ b/document-parser/requirements-local.txt @@ -1,3 +1,2 @@ -r requirements.txt docling>=2.80.0,<3.0.0 -docling-core>=2.0.0,<3.0.0 diff --git a/document-parser/requirements.txt b/document-parser/requirements.txt index 24aeedb..92dcf4c 100644 --- a/document-parser/requirements.txt +++ b/document-parser/requirements.txt @@ -1,3 +1,4 @@ +docling-core>=2.0.0,<3.0.0 fastapi>=0.115.0,<1.0.0 uvicorn[standard]>=0.32.0,<1.0.0 python-multipart>=0.0.12 diff --git a/document-parser/tests/test_bbox.py b/document-parser/tests/test_bbox.py index f201a73..8d3694f 100644 --- a/document-parser/tests/test_bbox.py +++ b/document-parser/tests/test_bbox.py @@ -8,7 +8,7 @@ misaligned overlays in the UI. import pytest from docling_core.types.doc.base import BoundingBox, CoordOrigin -from domain.bbox import EMPTY_BBOX, to_topleft_list +from infra.bbox import EMPTY_BBOX, to_topleft_list # --------------------------------------------------------------------------- # Standard conversions