File size: 8,330 Bytes
f5ef9fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""Docling document parsing with figure extraction and markdown export."""

from __future__ import annotations

import os
import uuid
from collections.abc import Callable
from pathlib import Path
from typing import Any

from storage import get_temp_dir


def _top_class(figure: Any) -> str | None:
    """Return the highest-confidence Docling classification label for a picture item.

    Tries ``figure.meta.classification.predictions`` first (docling-core ≥2.5),
    then falls back to the deprecated ``figure.annotations`` list.
    Returns ``None`` if classification is unavailable or fails.
    """
    try:
        meta = getattr(figure, "meta", None)
        if meta is not None:
            classification = getattr(meta, "classification", None)
            if classification is not None:
                preds = getattr(classification, "predictions", None)
                if preds:
                    best = max(preds, key=lambda p: p.confidence)
                    return best.class_name
    except Exception:  # noqa: BLE001
        pass

    try:
        for ann in getattr(figure, "annotations", []):
            preds = getattr(ann, "predicted_classes", None)
            if preds:
                best = max(preds, key=lambda p: p.confidence)
                return best.class_name
    except Exception:  # noqa: BLE001
        pass

    return None


_EXT_TO_INPUT_FORMAT = {
    ".pdf": "PDF",
    ".docx": "DOCX",
    ".xlsx": "XLSX",
    ".pptx": "PPTX",
}

_converter: Any = None


def _is_zerogpu_space() -> bool:
    """Detect HuggingFace ZeroGPU Spaces where CUDA init must happen inside @spaces.GPU."""
    return bool(os.environ.get("SPACE_ID"))


def _pick_accelerator_device() -> Any:
    """Return the best AcceleratorDevice for Docling, or None if unsupported."""
    try:
        from docling.datamodel.pipeline_options import AcceleratorDevice
    except ImportError:
        return None

    if _is_zerogpu_space():
        return AcceleratorDevice.CPU

    try:
        import torch
        if torch.cuda.is_available():
            return AcceleratorDevice.CUDA
        if torch.backends.mps.is_available():
            return AcceleratorDevice.MPS
    except ImportError:
        pass

    return AcceleratorDevice.CPU


def get_converter() -> Any:
    """Return a shared DocumentConverter instance, creating it on first call."""
    global _converter  # noqa: PLW0603
    if _converter is not None:
        return _converter

    from docling.datamodel.base_models import InputFormat
    from docling.document_converter import DocumentConverter, PdfFormatOption

    pdf_format_option = PdfFormatOption()
    pdf_format_option.pipeline_options.generate_picture_images = True
    pdf_format_option.pipeline_options.images_scale = 2.0
    pdf_format_option.pipeline_options.do_picture_classification = True

    device = _pick_accelerator_device()
    if device is not None:
        try:
            from docling.datamodel.pipeline_options import AcceleratorOptions
            pdf_format_option.pipeline_options.accelerator_options = AcceleratorOptions(
                device=device,
            )
        except Exception:  # noqa: BLE001
            pass

    # On ZeroGPU, hide CUDA from sub-libraries (RapidOCR, etc.) that do their
    # own torch.cuda detection and trigger real CUDA init.
    hide_cuda = _is_zerogpu_space()
    old_val = os.environ.get("CUDA_VISIBLE_DEVICES")
    if hide_cuda:
        os.environ["CUDA_VISIBLE_DEVICES"] = ""

    try:
        _converter = DocumentConverter(format_options={InputFormat.PDF: pdf_format_option})
    finally:
        if hide_cuda:
            if old_val is None:
                os.environ.pop("CUDA_VISIBLE_DEVICES", None)
            else:
                os.environ["CUDA_VISIBLE_DEVICES"] = old_val

    return _converter


def parse_document(
    file_bytes: bytes,
    file_ext: str = ".pdf",
    on_progress: Callable[[str], None] | None = None,
) -> dict[str, Any]:
    """Parse a document with Docling and extract markdown, text, and figure regions.

    Args:
        file_bytes: Document file content as bytes.
        file_ext: File extension (e.g. ``".pdf"``, ``".docx"``, ``".xlsx"``, ``".pptx"``).
        on_progress: Optional callback ``(phase_message) -> None`` for progress reporting.

    Returns:
        Dictionary with keys:
            - ``html``: HTML-wrapped markdown representation of the document.
            - ``text``: Full extracted plain text.
            - ``figures``: List of figure dicts with ``bbox``, ``page``, ``caption``, ``image``,
              and ``figure_type`` (top Docling classification label or ``None``).
    """
    def _report(msg: str) -> None:
        if on_progress:
            on_progress(msg)

    try:
        from docling.datamodel.base_models import InputFormat

        ext = file_ext.lower()
        tmp_path = str(get_temp_dir() / f"{uuid.uuid4().hex}{ext}")
        Path(tmp_path).write_bytes(file_bytes)

        try:
            format_name = _EXT_TO_INPUT_FORMAT.get(ext, "PDF")
            input_format = getattr(InputFormat, format_name)

            _report("Initializing document converter...")

            converter = get_converter()

            _report("Converting document (this may take a moment)...")
            result = converter.convert(tmp_path)
            doc = result.document

            _report("Exporting document content...")
            markdown_text = doc.export_to_markdown()
            html = markdown_text
            text = doc.export_to_text()

            _report("Processing figures...")
            figures: list[dict[str, Any]] = []
            try:
                if hasattr(doc, "pictures"):
                    for figure in doc.pictures:
                        if figure.content_layer.value != "body":
                            continue

                        page_num = 0
                        bbox_list = None

                        if figure.prov:
                            page_num = figure.prov[0].page_no - 1  # Docling is 1-based
                            bbox = figure.prov[0].bbox
                            bbox_list = [bbox.l, bbox.t, bbox.width, bbox.height]

                        caption = ""
                        if figure.captions:
                            for cap_ref in figure.captions:
                                try:
                                    if hasattr(cap_ref, "cref") and cap_ref.cref.startswith("#/texts/"):
                                        idx = int(cap_ref.cref.split("/")[-1])
                                        if idx < len(doc.texts):
                                            caption = doc.texts[idx].text
                                            break
                                except Exception:  # noqa: BLE001
                                    pass

                        if figure.image:
                            try:
                                pil_image = figure.image.pil_image
                                figures.append({
                                    "bbox": bbox_list,
                                    "page": page_num,
                                    "caption": caption,
                                    "image": pil_image,
                                    "figure_type": _top_class(figure),
                                })
                            except Exception:  # noqa: BLE001
                                pass

            except Exception:  # noqa: BLE001
                figures = []

            return {"html": html, "text": text, "figures": figures}

        finally:
            if os.path.exists(tmp_path):
                os.unlink(tmp_path)

    except ImportError as e:
        print(f"Docling import error: {e}, using placeholder")
        return {
            "html": "<h1>Sample Document</h1><p>Docling not available - using placeholder.</p>",
            "text": "Sample text from PDF.\n\nDocling not available - using placeholder.",
            "figures": [],
        }
    except Exception as e:  # noqa: BLE001
        import traceback

        print(f"Docling parse error: {e}")
        traceback.print_exc()
        return {
            "html": f"<h1>Error</h1><pre>{e!s}</pre>",
            "text": f"Error: {e!s}",
            "figures": [],
        }