"""Local Docling chunker — runs chunking in-process using docling-core. This adapter implements the DocumentChunker port. It deserializes a DoclingDocument from JSON, applies the requested chunker, and returns domain ChunkResult objects. """ from __future__ import annotations import asyncio import json import logging 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.value_objects import ChunkBbox, ChunkingOptions, ChunkResult from infra.bbox import EMPTY_BBOX, to_topleft_list logger = logging.getLogger(__name__) def _chunk_sync(document_json: str, options: ChunkingOptions) -> list[ChunkResult]: if not document_json or not document_json.strip(): raise ValueError("Empty document JSON — nothing to chunk") try: doc_data = json.loads(document_json) except json.JSONDecodeError as e: raise ValueError(f"Malformed document JSON: {e}") from e doc = DoclingDocument.model_validate(doc_data) chunker = _build_chunker(options) results: list[ChunkResult] = [] for chunk in chunker.chunk(doc): source_page = None token_count = 0 bboxes: list[ChunkBbox] = [] if hasattr(chunk, "meta") and chunk.meta and chunk.meta.doc_items: for doc_item in chunk.meta.doc_items: if not hasattr(doc_item, "prov") or not doc_item.prov: continue for prov in doc_item.prov: page_no = prov.page_no if source_page is None: source_page = page_no if prov.bbox: page_obj = doc.pages.get(page_no) if page_obj: bbox = to_topleft_list(prov.bbox, page_obj.size.height) if bbox != EMPTY_BBOX: bboxes.append(ChunkBbox(page=page_no, bbox=bbox)) if hasattr(chunker, "tokenizer") and chunker.tokenizer: token_count = chunker.tokenizer.count_tokens(chunk.text) headings = list(chunk.meta.headings) if chunk.meta and chunk.meta.headings else [] results.append( ChunkResult( text=chunk.text, headings=headings, source_page=source_page, token_count=token_count, bboxes=bboxes, ) ) logger.info("Chunked document into %d chunks (chunker=%s)", len(results), options.chunker_type) return results def _build_chunker(options: ChunkingOptions) -> HierarchicalChunker | HybridChunker: if options.chunker_type == "hierarchical": return HierarchicalChunker() return HybridChunker( max_tokens=options.max_tokens, merge_peers=options.merge_peers, repeat_table_header=options.repeat_table_header, ) class LocalChunker: """Adapter that runs docling-core chunking locally.""" async def chunk( self, document_json: str, options: ChunkingOptions, ) -> list[ChunkResult]: return await asyncio.to_thread(_chunk_sync, document_json, options)