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:
parent
6a791cec48
commit
ab6d42aecd
10 changed files with 25 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
-r requirements.txt
|
||||
docling>=2.80.0,<3.0.0
|
||||
docling-core>=2.0.0,<3.0.0
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue