Send a PDF to GROBID and return structured content. Args: input_path: Path to the PDF file. coordinates: If ``True``, include bounding-box coordinate strings in each passage (needed for PDF highlighting). Returns: dict or None: A
(self, input_path, coordinates=False)
| 157 | self.grobid_client = grobid_client |
| 158 | |
| 159 | def process_structure(self, input_path, coordinates=False): |
| 160 | """Send a PDF to GROBID and return structured content. |
| 161 | |
| 162 | Args: |
| 163 | input_path: Path to the PDF file. |
| 164 | coordinates: If ``True``, include bounding-box coordinate |
| 165 | strings in each passage (needed for PDF highlighting). |
| 166 | |
| 167 | Returns: |
| 168 | dict or None: A dict with keys: |
| 169 | |
| 170 | - ``"biblio"`` — bibliographic metadata (title, authors, DOI, …). |
| 171 | - ``"passages"`` — list of passage dicts, each containing |
| 172 | ``text``, ``type``, ``section``, ``subSection``, |
| 173 | ``passage_id``, and ``coordinates``. |
| 174 | - ``"filename"`` — stem of the PDF filename. |
| 175 | |
| 176 | Returns ``None`` if GROBID returns a non-200 status. |
| 177 | """ |
| 178 | try: |
| 179 | pdf_file, status, text = self.grobid_client.process_pdf( |
| 180 | "processFulltextDocument", |
| 181 | input_path, |
| 182 | consolidate_header=True, |
| 183 | consolidate_citations=False, |
| 184 | segment_sentences=False, |
| 185 | tei_coordinates=coordinates, |
| 186 | include_raw_citations=False, |
| 187 | include_raw_affiliations=False, |
| 188 | generateIDs=True, |
| 189 | ) |
| 190 | except requests.exceptions.RequestException as exc: |
| 191 | # Transport-level failure (connection refused, timeout, …). |
| 192 | # Local/usage errors (bad path, parsing bugs) are intentionally |
| 193 | # not caught here so they surface with their real traceback. |
| 194 | raise GrobidServiceError("Grobid service did not respond.") from exc |
| 195 | |
| 196 | if status != 200: |
| 197 | # Grobid attaches a human-readable reason to error responses |
| 198 | # (e.g. a 500 body explaining what went wrong). Surface it |
| 199 | # alongside the status code instead of discarding it. |
| 200 | reason = text.strip() if text else "" |
| 201 | message = f"Grobid service returned status {status}." |
| 202 | if reason: |
| 203 | message += f" {reason}" |
| 204 | raise GrobidServiceError(message, status_code=status) |
| 205 | |
| 206 | # Grobid can answer 200 with an empty body (e.g. it gave up on the PDF). |
| 207 | if not text or not text.strip(): |
| 208 | raise GrobidServiceError("Grobid returned an empty response.", status_code=status) |
| 209 | |
| 210 | # A truncated/corrupted TEI payload makes the XML parser blow up; map |
| 211 | # that to a clear service error instead of an opaque parsing traceback. |
| 212 | try: |
| 213 | document_object = self.parse_grobid_xml(text, coordinates=coordinates) |
| 214 | except GrobidServiceError: |
| 215 | raise |
| 216 | except Exception as exc: |
no test coverage detected