HarshShinde0
Optimize UI styling, enable GPU acceleration, native chat completions, and remove thinking panel
0a40c65 | from docling.document_converter import DocumentConverter, PdfFormatOption | |
| from docling.datamodel.base_models import InputFormat | |
| from docling.datamodel.pipeline_options import PdfPipelineOptions | |
| from docling_core.transforms.chunker.hybrid_chunker import HybridChunker | |
| from transformers import AutoTokenizer | |
| import pdfplumber | |
| import logging | |
| import re | |
| logger = logging.getLogger(__name__) | |
| class DoclingParser: | |
| """Uses IBM Docling's advanced structural parsing and HybridChunker.""" | |
| def __init__(self, tokenizer_model: str, max_tokens: int, ocr_mode: str): | |
| self.ocr_mode = (ocr_mode or "").lower() | |
| if self.ocr_mode not in {"never", "auto", "always"}: | |
| raise ValueError("rag.ocr_mode must be one of: never, auto, always") | |
| # Non-OCR parser for fast PDFs with embedded text. | |
| no_ocr_options = PdfPipelineOptions( | |
| do_ocr=False, | |
| generate_picture_images=False, | |
| # Reduce memory bloat | |
| layout_batch_size=2, | |
| table_batch_size=2 | |
| ) | |
| self.no_ocr_converter = DocumentConverter( | |
| format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=no_ocr_options)} | |
| ) | |
| # OCR parser for scanned/badly encoded PDFs. | |
| ocr_options = PdfPipelineOptions( | |
| do_ocr=True, | |
| generate_picture_images=False, | |
| # Aggressively reduce batch sizes to prevent OOM | |
| ocr_batch_size=1, | |
| layout_batch_size=1, | |
| table_batch_size=1, | |
| queue_max_size=2 | |
| ) | |
| self.ocr_converter = DocumentConverter( | |
| format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=ocr_options)} | |
| ) | |
| # 2. Setup IBM's HybridChunker (Chunks structurally by tables/headings, not blindly by character count) | |
| try: | |
| tokenizer = AutoTokenizer.from_pretrained(tokenizer_model) | |
| self.chunker = HybridChunker(tokenizer=tokenizer, max_tokens=max_tokens) | |
| except Exception as e: | |
| logger.warning( | |
| f"Could not load tokenizer {tokenizer_model}. " | |
| f"Falling back to raw document text chunking. Error: {e}" | |
| ) | |
| self.chunker = None | |
| logger.info("Initialized Advanced Docling Parser & HybridChunker.") | |
| def parse_and_chunk(self, file_path: str) -> list[str]: | |
| """Parses a PDF and returns a list of structurally intact text/table chunks.""" | |
| logger.info(f"Parsing structure for: {file_path}") | |
| # Test standard fast extraction first to preserve natural layout | |
| # (e.g., forms where Docling's layout AI incorrectly splits columns) | |
| fast_text = self._extract_with_pdfplumber(file_path) | |
| fast_chunks = [fast_text] if fast_text else [] | |
| if self.ocr_mode == "never": | |
| return fast_chunks if fast_chunks and not self._is_likely_garbled(fast_chunks) else self._parse_with_converter(self.no_ocr_converter, file_path) | |
| if self.ocr_mode == "always": | |
| return self._parse_with_converter(self.ocr_converter, file_path) | |
| # Auto mode: Use pdfplumber if it yields clean layout-preserved text. | |
| if fast_chunks and not self._is_likely_garbled(fast_chunks): | |
| logger.info("Auto mode: pdfplumber successfully extracted clean text format.") | |
| return fast_chunks | |
| # Fallback to Docling non-OCR, then OCR if still garbled. | |
| logger.warning(f"Auto mode: fast extraction garbled or empty. Trying Docling for: {file_path}") | |
| chunks = self._parse_with_converter(self.no_ocr_converter, file_path) | |
| if chunks and not self._is_likely_garbled(chunks): | |
| return chunks | |
| logger.warning(f"Auto OCR fallback triggered (running layout and OCR models) for: {file_path}") | |
| ocr_chunks = self._parse_with_converter(self.ocr_converter, file_path) | |
| return ocr_chunks if ocr_chunks else chunks | |
| def _parse_with_converter(self, converter, file_path: str) -> list[str]: | |
| try: | |
| result = converter.convert(file_path) | |
| return self._doc_to_chunks(result) | |
| except Exception as e: | |
| logger.warning(f"Docling conversion failed for {file_path}; using pdfplumber fallback. Error: {e}") | |
| text = self._extract_with_pdfplumber(file_path) | |
| return [text] if text else [] | |
| def _doc_to_chunks(self, result) -> list[str]: | |
| # To guarantee no text is dropped or limited by the chunking process, | |
| # we extract the complete document as Markdown from A-Z. | |
| # This will then be passed to the text splitters to split exhaustively. | |
| if hasattr(result.document, "export_to_markdown"): | |
| return [result.document.export_to_markdown()] | |
| elif hasattr(result.document, "export_to_text"): | |
| return [result.document.export_to_text()] | |
| return [str(result.document)] | |
| def _is_likely_garbled(self, chunks: list[str], threshold: float = 0.08) -> bool: | |
| text = "\n".join(chunks or []) | |
| if not text: | |
| return True | |
| # Very short text can be sparse/noisy naturally; avoid aggressive OCR fallback. | |
| if len(text) < 400: | |
| return False | |
| total = len(text) | |
| control_like = 0 | |
| for ch in text: | |
| code = ord(ch) | |
| if code < 32 and ch not in ("\n", "\r", "\t"): | |
| control_like += 1 | |
| elif 127 <= code <= 159: | |
| control_like += 1 | |
| control_ratio = control_like / total | |
| # If there are enough readable words, keep non-OCR output even with symbols. | |
| word_count = len(re.findall(r"[A-Za-z]{2,}", text)) | |
| has_readable_text = word_count >= 40 | |
| if has_readable_text: | |
| return False | |
| return control_ratio >= threshold | |
| def _extract_with_pdfplumber(self, file_path: str) -> str: | |
| parts = [] | |
| with pdfplumber.open(file_path) as pdf: | |
| for page in pdf.pages: | |
| page_text = page.extract_text() or "" | |
| if page_text.strip(): | |
| parts.append(page_text) | |
| return "\n\n".join(parts).strip() | |