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.
This commit is contained in:
Pier-Jean Malandrino 2026-04-03 13:17:26 +02:00
parent 6a791cec48
commit ab6d42aecd
10 changed files with 25 additions and 22 deletions

View file

@ -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

View file

@ -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 |

View file

@ -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.

View file

@ -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__)

View file

@ -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,

View file

@ -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)

View file

@ -1,3 +1,2 @@
-r requirements.txt
docling>=2.80.0,<3.0.0
docling-core>=2.0.0,<3.0.0

View file

@ -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

View file

@ -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