grobid-client-python 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/.github/workflows/ci-build.yml +5 -1
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/PKG-INFO +28 -1
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/Readme.md +27 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/format/TEI2LossyJSON.py +80 -26
- grobid_client_python-0.1.4/grobid_client/format/TEI2LossyJSON_cli.py +131 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/format/TEI2Markdown.py +82 -27
- grobid_client_python-0.1.4/grobid_client/format/TEI2Markdown_cli.py +130 -0
- grobid_client_python-0.1.4/grobid_client/format/__main__.py +48 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/grobid_client.py +21 -1
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client_python.egg-info/PKG-INFO +28 -1
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client_python.egg-info/SOURCES.txt +9 -1
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/pyproject.toml +1 -1
- grobid_client_python-0.1.4/resources/test_pdf/mjb3wlzxcb2mc-migowebupload-1766042162782.grobid.tei.xml +1053 -0
- grobid_client_python-0.1.4/tests/resources/article_withdrawn.grobid.tei.xml +51 -0
- grobid_client_python-0.1.4/tests/resources/repeated_text/ijms-24-05988.grobid.tei.xml +1298 -0
- grobid_client_python-0.1.4/tests/resources/repeated_text/ijms-24-05988.json +1459 -0
- grobid_client_python-0.1.4/tests/resources/repeated_text/ijms-24-05988.md +264 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/test_conversions.py +301 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/.github/workflows/ci-release.yml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/.gitignore +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/LICENSE +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/MANIFEST.in +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/config.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/example.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/__init__.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/client.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/format/__init__.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/format/validate_json_refs.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client_python.egg-info/dependency_links.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client_python.egg-info/entry_points.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client_python.egg-info/requires.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client_python.egg-info/top_level.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/pytest.ini +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/requirements.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/20180928112135.png +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/20180928112138.csv +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_pdf/0046d83a-edd6-4631-b57c-755cdcce8b7f.pdf +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_pdf/0046d83a-edd6-4631-b57c-755cdcce8b7f.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-1.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-2.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-3.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-4.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-5.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-6.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/resources/test_txt/reference-strings-7.txt +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/setup.cfg +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/__init__.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/conftest.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/0046d83a-edd6-4631-b57c-755cdcce8b7f.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/__init__.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1038_s41477-023-01501-1.grobid.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1038_s41477-023-01501-1.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1038_s41586-023-05895-y.grobid.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1038_s41586-023-05895-y.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1038_s41598-023-32039-z.grobid.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1038_s41598-023-32039-z.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1371_journal.pone.0218311.grobid.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.1371_journal.pone.0218311.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.7554_elife.78558.grobid.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/10.7554_elife.78558.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/resources/refs_offsets/2021.naacl-main.224.grobid.tei.xml +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/run_tests.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/test_client.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/test_config.json +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/test_grobid_client.py +0 -0
- {grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/tests/test_integration.py +0 -0
|
@@ -22,7 +22,11 @@ jobs:
|
|
|
22
22
|
python-version: ${{ matrix.python-version }}
|
|
23
23
|
cache: 'pip'
|
|
24
24
|
- name: Cleanup more disk space
|
|
25
|
-
run:
|
|
25
|
+
run: |
|
|
26
|
+
sudo rm -rf /usr/share/dotnet
|
|
27
|
+
sudo rm -rf /opt/ghc
|
|
28
|
+
sudo rm -rf "/usr/local/share/boost"
|
|
29
|
+
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
|
26
30
|
- name: Install dependencies
|
|
27
31
|
run: |
|
|
28
32
|
python -m pip install --upgrade pip
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grobid-client-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Simple python client for GROBID REST services
|
|
5
5
|
Author-email: Patrice Lopez <patrice.lopez@science-miner.com>
|
|
6
6
|
Maintainer-email: Patrice Lopez <patrice.lopez@science-miner.com>, Luca Foppiano <lucanoro@duck.com>
|
|
@@ -443,6 +443,7 @@ client.process(
|
|
|
443
443
|
markdown_output=True
|
|
444
444
|
)
|
|
445
445
|
|
|
446
|
+
```python
|
|
446
447
|
# Process citation lists
|
|
447
448
|
client.process(
|
|
448
449
|
service="processCitationList",
|
|
@@ -451,6 +452,32 @@ client.process(
|
|
|
451
452
|
)
|
|
452
453
|
```
|
|
453
454
|
|
|
455
|
+
### Standalone Conversion Tools
|
|
456
|
+
|
|
457
|
+
The library includes standalone scripts to convert TEI XML files to other formats without using the main client or server.
|
|
458
|
+
|
|
459
|
+
#### TEI to JSON Converter
|
|
460
|
+
|
|
461
|
+
Converts TEI XML files to the structured JSON format (similar to `--json` option).
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
# Convert a single file
|
|
465
|
+
python -m grobid_client.format.TEI2LossyJSON_cli --input path/to/file.tei.xml --output path/to/output.json
|
|
466
|
+
|
|
467
|
+
# Convert with verbose logging
|
|
468
|
+
python -m grobid_client.format.TEI2LossyJSON_cli --input path/to/file.tei.xml --verbose
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### TEI to Markdown Converter
|
|
472
|
+
|
|
473
|
+
Converts TEI XML files to Markdown format (similar to `--markdown` option).
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# Convert a single file
|
|
477
|
+
python -m grobid_client.format.TEI2Markdown_cli --input path/to/file.tei.xml --output path/to/output.md
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
|
|
454
481
|
## ⚙️ Configuration
|
|
455
482
|
|
|
456
483
|
Configuration can be provided via a JSON file. When using the CLI, the `--server` argument overrides the config file
|
|
@@ -224,6 +224,7 @@ client.process(
|
|
|
224
224
|
markdown_output=True
|
|
225
225
|
)
|
|
226
226
|
|
|
227
|
+
```python
|
|
227
228
|
# Process citation lists
|
|
228
229
|
client.process(
|
|
229
230
|
service="processCitationList",
|
|
@@ -232,6 +233,32 @@ client.process(
|
|
|
232
233
|
)
|
|
233
234
|
```
|
|
234
235
|
|
|
236
|
+
### Standalone Conversion Tools
|
|
237
|
+
|
|
238
|
+
The library includes standalone scripts to convert TEI XML files to other formats without using the main client or server.
|
|
239
|
+
|
|
240
|
+
#### TEI to JSON Converter
|
|
241
|
+
|
|
242
|
+
Converts TEI XML files to the structured JSON format (similar to `--json` option).
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# Convert a single file
|
|
246
|
+
python -m grobid_client.format.TEI2LossyJSON_cli --input path/to/file.tei.xml --output path/to/output.json
|
|
247
|
+
|
|
248
|
+
# Convert with verbose logging
|
|
249
|
+
python -m grobid_client.format.TEI2LossyJSON_cli --input path/to/file.tei.xml --verbose
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### TEI to Markdown Converter
|
|
253
|
+
|
|
254
|
+
Converts TEI XML files to Markdown format (similar to `--markdown` option).
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Convert a single file
|
|
258
|
+
python -m grobid_client.format.TEI2Markdown_cli --input path/to/file.tei.xml --output path/to/output.md
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
|
|
235
262
|
## ⚙️ Configuration
|
|
236
263
|
|
|
237
264
|
Configuration can be provided via a JSON file. When using the CLI, the `--server` argument overrides the config file
|
{grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/format/TEI2LossyJSON.py
RENAMED
|
@@ -9,6 +9,8 @@ import os
|
|
|
9
9
|
import uuid
|
|
10
10
|
from collections import OrderedDict
|
|
11
11
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
12
|
+
import html
|
|
13
|
+
import re
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Dict, Union, BinaryIO, Iterator
|
|
14
16
|
|
|
@@ -41,8 +43,15 @@ class TEI2LossyJSONConverter:
|
|
|
41
43
|
If stream=False returns the full document dict (same shape as original function).
|
|
42
44
|
"""
|
|
43
45
|
# Load with BeautifulSoup but avoid building huge structures when streaming
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
if hasattr(tei_file, 'read'):
|
|
47
|
+
# File-like object (BinaryIO/StringIO)
|
|
48
|
+
content = tei_file.read()
|
|
49
|
+
if isinstance(content, bytes):
|
|
50
|
+
content = content.decode('utf-8')
|
|
51
|
+
else:
|
|
52
|
+
# Path-like object
|
|
53
|
+
with open(tei_file, 'r', encoding='utf-8') as f:
|
|
54
|
+
content = f.read()
|
|
46
55
|
soup = BeautifulSoup(content, 'xml')
|
|
47
56
|
|
|
48
57
|
if soup.TEI is None:
|
|
@@ -222,7 +231,6 @@ class TEI2LossyJSONConverter:
|
|
|
222
231
|
Extract detailed bibliographic information from TEI biblStruct elements.
|
|
223
232
|
Implements comprehensive parsing for all standard TEI bibliographic components.
|
|
224
233
|
"""
|
|
225
|
-
import re
|
|
226
234
|
|
|
227
235
|
citation_data = OrderedDict()
|
|
228
236
|
citation_data['id'] = f"b{index}"
|
|
@@ -430,7 +438,6 @@ class TEI2LossyJSONConverter:
|
|
|
430
438
|
|
|
431
439
|
def _process_imprint_details(self, imprint_element: Tag, publication_metadata: Dict):
|
|
432
440
|
"""Extract and process imprint information including publisher, dates, and page ranges."""
|
|
433
|
-
import re
|
|
434
441
|
|
|
435
442
|
# Extract publisher information
|
|
436
443
|
publisher_elements = imprint_element.find_all("publisher")
|
|
@@ -557,7 +564,6 @@ class TEI2LossyJSONConverter:
|
|
|
557
564
|
Extract person data (author/editor) from TEI persName or author elements.
|
|
558
565
|
Handles various name formats and affiliations.
|
|
559
566
|
"""
|
|
560
|
-
import re
|
|
561
567
|
|
|
562
568
|
person_data = {}
|
|
563
569
|
|
|
@@ -628,11 +634,9 @@ class TEI2LossyJSONConverter:
|
|
|
628
634
|
text = text.decode('utf-8', errors='ignore')
|
|
629
635
|
|
|
630
636
|
# Normalize whitespace and strip
|
|
631
|
-
import re
|
|
632
637
|
text = re.sub(r'\s+', ' ', text.strip())
|
|
633
638
|
|
|
634
639
|
# Remove any potential XML/HTML entities
|
|
635
|
-
import html
|
|
636
640
|
text = html.unescape(text)
|
|
637
641
|
|
|
638
642
|
return text
|
|
@@ -665,14 +669,33 @@ class TEI2LossyJSONConverter:
|
|
|
665
669
|
|
|
666
670
|
div_type = div.get("type")
|
|
667
671
|
|
|
672
|
+
# Check if this is a header-only div (no content, no nested divs)
|
|
673
|
+
# If so, capture its header as context for subsequent divs
|
|
674
|
+
head = div.find("head")
|
|
675
|
+
direct_p_nodes = [c for c in div.children if hasattr(c, 'name') and c.name == "p"]
|
|
676
|
+
direct_formula_nodes = [c for c in div.children if hasattr(c, 'name') and c.name == "formula"]
|
|
677
|
+
nested_divs = [c for c in div.children if hasattr(c, 'name') and (c.name == "div" or (c.name and c.name.endswith(":div")))]
|
|
678
|
+
has_direct_content = len(direct_p_nodes) > 0 or len(direct_formula_nodes) > 0
|
|
679
|
+
|
|
680
|
+
if head and not has_direct_content and len(nested_divs) == 0:
|
|
681
|
+
# This is a header-only div with no nested content
|
|
682
|
+
# Capture the header for the next div
|
|
683
|
+
head_paragraph = self._clean_text(head.get_text())
|
|
684
|
+
continue # Skip to next div, the header will be used by subsequent sibling
|
|
685
|
+
|
|
668
686
|
# Process this div and potentially nested divs
|
|
669
687
|
for passage in self._process_div_with_nested_content(div, passage_level, head_paragraph):
|
|
670
688
|
yield passage
|
|
689
|
+
|
|
690
|
+
# Reset head_paragraph after it's been used by a content-bearing div
|
|
691
|
+
head_paragraph = None
|
|
692
|
+
|
|
671
693
|
|
|
672
694
|
def _process_div_with_nested_content(self, div: Tag, passage_level: str, head_paragraph: str = None) -> Iterator[Dict[str, Union[str, Dict[str, str]]]]:
|
|
673
695
|
"""
|
|
674
696
|
Process a div and its nested content, handling various back section types.
|
|
675
697
|
Supports nested divs for complex back sections like annex with multiple subsections.
|
|
698
|
+
Also handles formula elements that are direct children of divs.
|
|
676
699
|
"""
|
|
677
700
|
head = div.find("head")
|
|
678
701
|
p_nodes = div.find_all("p")
|
|
@@ -687,10 +710,12 @@ class TEI2LossyJSONConverter:
|
|
|
687
710
|
if child.name == "div" or child.name.endswith(":div"):
|
|
688
711
|
nested_divs.append(child)
|
|
689
712
|
|
|
690
|
-
# Count only direct child paragraphs, not those in nested divs
|
|
713
|
+
# Count only direct child paragraphs and formulas, not those in nested divs
|
|
691
714
|
direct_p_nodes = [child for child in div.children if hasattr(child, 'name') and child.name == "p"]
|
|
715
|
+
direct_formula_nodes = [child for child in div.children if hasattr(child, 'name') and child.name == "formula"]
|
|
716
|
+
has_direct_content = len(direct_p_nodes) > 0 or len(direct_formula_nodes) > 0
|
|
692
717
|
|
|
693
|
-
if len(nested_divs) > 0 and
|
|
718
|
+
if len(nested_divs) > 0 and not has_direct_content:
|
|
694
719
|
# This is a container div - process each nested div independently
|
|
695
720
|
for nested_div in nested_divs:
|
|
696
721
|
# Skip references divs
|
|
@@ -703,11 +728,11 @@ class TEI2LossyJSONConverter:
|
|
|
703
728
|
|
|
704
729
|
# Determine the section header and content type for divs with content
|
|
705
730
|
if head:
|
|
706
|
-
if
|
|
707
|
-
# This div has only a head, no paragraphs (standalone head)
|
|
731
|
+
if not has_direct_content:
|
|
732
|
+
# This div has only a head, no paragraphs or formulas (standalone head)
|
|
708
733
|
current_head_paragraph = self._clean_text(head.get_text())
|
|
709
734
|
else:
|
|
710
|
-
# This div has both head and
|
|
735
|
+
# This div has both head and content - head is the section header
|
|
711
736
|
head_section = self._clean_text(head.get_text())
|
|
712
737
|
else:
|
|
713
738
|
# If no head element, try to use the type attribute as head_section
|
|
@@ -722,7 +747,7 @@ class TEI2LossyJSONConverter:
|
|
|
722
747
|
head_section = "Author Contributions"
|
|
723
748
|
elif div_type == "availability":
|
|
724
749
|
# Only set as default if this div has its own content
|
|
725
|
-
if
|
|
750
|
+
if has_direct_content:
|
|
726
751
|
head_section = "Data Availability"
|
|
727
752
|
elif div_type == "annex":
|
|
728
753
|
head_section = "Annex"
|
|
@@ -730,27 +755,60 @@ class TEI2LossyJSONConverter:
|
|
|
730
755
|
# Generic handling - capitalize and format
|
|
731
756
|
head_section = div_type.replace("_", " ").title()
|
|
732
757
|
|
|
733
|
-
# Process paragraphs in
|
|
734
|
-
|
|
735
|
-
|
|
758
|
+
# Process direct children (paragraphs and formulas) in document order
|
|
759
|
+
for child in div.children:
|
|
760
|
+
if not hasattr(child, 'name') or not child.name:
|
|
761
|
+
continue
|
|
762
|
+
|
|
763
|
+
if child.name == "p":
|
|
736
764
|
paragraph_id = get_random_id(prefix="p_")
|
|
737
765
|
|
|
738
766
|
if passage_level == "sentence":
|
|
739
|
-
for id_s, sentence in enumerate(
|
|
767
|
+
for id_s, sentence in enumerate(child.find_all("s")):
|
|
740
768
|
struct = get_formatted_passage(current_head_paragraph or head_paragraph, head_section, paragraph_id, sentence)
|
|
741
769
|
if self.validate_refs:
|
|
742
770
|
for ref in struct['refs']:
|
|
743
|
-
assert
|
|
744
|
-
assert
|
|
771
|
+
assert ref['offset_start'] < ref['offset_end'], "Wrong offsets"
|
|
772
|
+
assert struct['text'][ref['offset_start']:ref['offset_end']] == ref['text'], "Cannot apply offsets"
|
|
745
773
|
yield struct
|
|
746
774
|
else:
|
|
747
|
-
struct = get_formatted_passage(current_head_paragraph or head_paragraph, head_section, paragraph_id,
|
|
775
|
+
struct = get_formatted_passage(current_head_paragraph or head_paragraph, head_section, paragraph_id, child)
|
|
748
776
|
if self.validate_refs:
|
|
749
777
|
for ref in struct['refs']:
|
|
750
|
-
assert
|
|
751
|
-
assert
|
|
778
|
+
assert ref['offset_start'] < ref['offset_end'], "Wrong offsets"
|
|
779
|
+
assert struct['text'][ref['offset_start']:ref['offset_end']] == ref['text'], "Cannot apply offsets"
|
|
752
780
|
yield struct
|
|
753
781
|
|
|
782
|
+
elif child.name == "formula":
|
|
783
|
+
# Process formula elements as passages
|
|
784
|
+
formula_id = get_random_id(prefix="f_")
|
|
785
|
+
formula_text = self._clean_text(child.get_text())
|
|
786
|
+
|
|
787
|
+
if formula_text:
|
|
788
|
+
# Create a passage structure for the formula
|
|
789
|
+
formula_passage = {
|
|
790
|
+
"id": formula_id,
|
|
791
|
+
"text": formula_text,
|
|
792
|
+
"coords": [
|
|
793
|
+
box_to_dict(coord.split(","))
|
|
794
|
+
for coord in child.get("coords", "").split(";")
|
|
795
|
+
] if child.has_attr("coords") else [],
|
|
796
|
+
"refs": [],
|
|
797
|
+
"type": "formula"
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if current_head_paragraph or head_paragraph:
|
|
801
|
+
formula_passage["head_paragraph"] = current_head_paragraph or head_paragraph
|
|
802
|
+
if head_section:
|
|
803
|
+
formula_passage["head_section"] = head_section
|
|
804
|
+
|
|
805
|
+
# Extract formula label if present
|
|
806
|
+
label = child.find("label")
|
|
807
|
+
if label:
|
|
808
|
+
formula_passage["label"] = self._clean_text(label.get_text())
|
|
809
|
+
|
|
810
|
+
yield formula_passage
|
|
811
|
+
|
|
754
812
|
# Update head_paragraph for potential next div
|
|
755
813
|
if current_head_paragraph is not None:
|
|
756
814
|
head_paragraph = current_head_paragraph
|
|
@@ -817,8 +875,6 @@ def get_refs_with_offsets(element):
|
|
|
817
875
|
def _clean_text(text: str) -> str:
|
|
818
876
|
if not text:
|
|
819
877
|
return ""
|
|
820
|
-
import re
|
|
821
|
-
import html
|
|
822
878
|
text = re.sub(r'\s+', ' ', text.strip())
|
|
823
879
|
text = html.unescape(text)
|
|
824
880
|
return text
|
|
@@ -903,8 +959,6 @@ def get_formatted_passage(head_paragraph, head_section, paragraph_id, element):
|
|
|
903
959
|
def _clean_text_local(text: str) -> str:
|
|
904
960
|
if not text:
|
|
905
961
|
return ""
|
|
906
|
-
import re
|
|
907
|
-
import html
|
|
908
962
|
text = re.sub(r'\s+', ' ', text.strip())
|
|
909
963
|
text = html.unescape(text)
|
|
910
964
|
return text
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Standalone CLI for TEI2LossyJSON converter.
|
|
4
|
+
|
|
5
|
+
This script provides a command-line interface for converting TEI XML files to JSON format
|
|
6
|
+
using the TEI2LossyJSONConverter.
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .TEI2LossyJSON import TEI2LossyJSONConverter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup_logging(verbose: bool = False):
|
|
18
|
+
"""Setup logging configuration."""
|
|
19
|
+
level = logging.INFO if verbose else logging.WARNING
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=level,
|
|
22
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
23
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def convert_single_file(input_file: Path, output_file: Path, verbose: bool = False) -> bool:
|
|
28
|
+
"""Convert a single TEI file to JSON format."""
|
|
29
|
+
try:
|
|
30
|
+
if verbose:
|
|
31
|
+
logging.info(f"Converting {input_file} to {output_file}")
|
|
32
|
+
|
|
33
|
+
converter = TEI2LossyJSONConverter()
|
|
34
|
+
result = converter.convert_tei_file(input_file, stream=False)
|
|
35
|
+
|
|
36
|
+
if result is None:
|
|
37
|
+
logging.error(f"Failed to convert {input_file}: TEI file is not well-formed or empty")
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Ensure output directory exists
|
|
41
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
# Write JSON output
|
|
44
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
45
|
+
json.dump(result, f, indent=2, ensure_ascii=False)
|
|
46
|
+
|
|
47
|
+
if verbose:
|
|
48
|
+
logging.info(f"Successfully converted {input_file} to {output_file}")
|
|
49
|
+
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logging.error(f"Error converting {input_file}: {str(e)}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
"""Main CLI entry point."""
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
description="Convert TEI XML files to JSON format using TEI2LossyJSON converter",
|
|
61
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
62
|
+
epilog="""
|
|
63
|
+
Examples:
|
|
64
|
+
# Convert a single TEI file
|
|
65
|
+
python -m grobid_client.format.TEI2LossyJSON --input input.tei.xml --output output.json
|
|
66
|
+
|
|
67
|
+
# Convert with verbose logging
|
|
68
|
+
python -m grobid_client.format.TEI2LossyJSON --input input.tei.xml --output output.json --verbose
|
|
69
|
+
|
|
70
|
+
# Convert and output to stdout
|
|
71
|
+
python -m grobid_client.format.TEI2LossyJSON --input input.tei.xml
|
|
72
|
+
"""
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--input", "-i",
|
|
77
|
+
type=Path,
|
|
78
|
+
required=True,
|
|
79
|
+
help="Input TEI XML file to convert"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--output", "-o",
|
|
84
|
+
type=Path,
|
|
85
|
+
help="Output JSON file (if not specified, prints to stdout)"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--verbose", "-v",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help="Enable verbose logging"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
args = parser.parse_args()
|
|
95
|
+
|
|
96
|
+
# Setup logging
|
|
97
|
+
setup_logging(args.verbose)
|
|
98
|
+
|
|
99
|
+
# Validate input file
|
|
100
|
+
if not args.input.exists():
|
|
101
|
+
logging.error(f"Input file does not exist: {args.input}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
if not args.input.is_file():
|
|
105
|
+
logging.error(f"Input path is not a file: {args.input}")
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
# Convert the file
|
|
109
|
+
if args.output:
|
|
110
|
+
success = convert_single_file(args.input, args.output, args.verbose)
|
|
111
|
+
sys.exit(0 if success else 1)
|
|
112
|
+
else:
|
|
113
|
+
# Output to stdout
|
|
114
|
+
try:
|
|
115
|
+
converter = TEI2LossyJSONConverter()
|
|
116
|
+
result = converter.convert_tei_file(args.input, stream=False)
|
|
117
|
+
|
|
118
|
+
if result is None:
|
|
119
|
+
logging.error(f"Failed to convert {args.input}: TEI file is not well-formed or empty")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
# Print JSON to stdout
|
|
123
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logging.error(f"Error converting {args.input}: {str(e)}")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
main()
|
{grobid_client_python-0.1.2 → grobid_client_python-0.1.4}/grobid_client/format/TEI2Markdown.py
RENAMED
|
@@ -11,8 +11,7 @@ Markdown format with the following sections:
|
|
|
11
11
|
- Annex
|
|
12
12
|
- References
|
|
13
13
|
"""
|
|
14
|
-
import
|
|
15
|
-
import uuid
|
|
14
|
+
import re
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
from typing import List, Dict, Union, Optional, BinaryIO
|
|
18
17
|
from bs4 import BeautifulSoup, NavigableString, Tag
|
|
@@ -44,9 +43,12 @@ class TEI2MarkdownConverter:
|
|
|
44
43
|
try:
|
|
45
44
|
# Load with BeautifulSoup
|
|
46
45
|
if isinstance(tei_file, (str, Path)):
|
|
47
|
-
|
|
46
|
+
with open(tei_file, 'r', encoding='utf-8') as f:
|
|
47
|
+
content = f.read()
|
|
48
48
|
else:
|
|
49
49
|
content = tei_file.read()
|
|
50
|
+
if isinstance(content, bytes):
|
|
51
|
+
content = content.decode('utf-8')
|
|
50
52
|
|
|
51
53
|
soup = BeautifulSoup(content, 'xml')
|
|
52
54
|
|
|
@@ -77,7 +79,7 @@ class TEI2MarkdownConverter:
|
|
|
77
79
|
# Extract publication date
|
|
78
80
|
pub_date = self._extract_publication_date(soup)
|
|
79
81
|
if pub_date:
|
|
80
|
-
markdown_sections.append(f"
|
|
82
|
+
markdown_sections.append(f"Published on {pub_date}\n\n")
|
|
81
83
|
|
|
82
84
|
# Extract abstract
|
|
83
85
|
abstract = self._extract_abstract(soup)
|
|
@@ -211,14 +213,24 @@ class TEI2MarkdownConverter:
|
|
|
211
213
|
head = div.find("head")
|
|
212
214
|
if head:
|
|
213
215
|
section_title = head.get_text().strip()
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if
|
|
221
|
-
|
|
216
|
+
if section_title:
|
|
217
|
+
fulltext_sections.append(f"### {section_title}\n")
|
|
218
|
+
|
|
219
|
+
# Process direct children of the div in document order
|
|
220
|
+
# This captures paragraphs, formulas, and other elements as they appear
|
|
221
|
+
for child in div.children:
|
|
222
|
+
if not hasattr(child, 'name') or not child.name:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if child.name == "p":
|
|
226
|
+
paragraph_text = self._process_paragraph(child)
|
|
227
|
+
if paragraph_text.strip():
|
|
228
|
+
fulltext_sections.append(f"{paragraph_text}\n\n")
|
|
229
|
+
elif child.name == "formula":
|
|
230
|
+
# Handle formula elements - extract text and optional label
|
|
231
|
+
formula_text = self._process_formula(child)
|
|
232
|
+
if formula_text.strip():
|
|
233
|
+
fulltext_sections.append(f"{formula_text}\n\n")
|
|
222
234
|
|
|
223
235
|
return "".join(fulltext_sections)
|
|
224
236
|
|
|
@@ -270,16 +282,23 @@ class TEI2MarkdownConverter:
|
|
|
270
282
|
if header_text not in annex_sections:
|
|
271
283
|
annex_sections.append(header_text)
|
|
272
284
|
|
|
273
|
-
# Process
|
|
285
|
+
# Process direct children of this div in document order
|
|
286
|
+
# This captures paragraphs, formulas, and other elements as they appear
|
|
274
287
|
for child in div.children:
|
|
275
|
-
if hasattr(child, 'name')
|
|
288
|
+
if not hasattr(child, 'name') or not child.name:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
if child.name == "p":
|
|
276
292
|
paragraph_text = self._process_paragraph(child)
|
|
277
293
|
if paragraph_text.strip():
|
|
278
294
|
annex_sections.append(f"{paragraph_text}\n\n")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
elif child.name == "formula":
|
|
296
|
+
# Handle formula elements
|
|
297
|
+
formula_text = self._process_formula(child)
|
|
298
|
+
if formula_text.strip():
|
|
299
|
+
annex_sections.append(f"{formula_text}\n\n")
|
|
300
|
+
elif child.name == "div":
|
|
301
|
+
# Process nested div elements
|
|
283
302
|
self._process_div_and_nested_divs(child, annex_sections)
|
|
284
303
|
|
|
285
304
|
def _extract_references(self, soup: BeautifulSoup) -> str:
|
|
@@ -336,6 +355,34 @@ class TEI2MarkdownConverter:
|
|
|
336
355
|
|
|
337
356
|
return "".join(text_parts).strip()
|
|
338
357
|
|
|
358
|
+
def _process_formula(self, formula_element: Tag) -> str:
|
|
359
|
+
"""Process a formula element and convert to markdown.
|
|
360
|
+
|
|
361
|
+
Formulas are rendered as italicized text with optional equation label.
|
|
362
|
+
"""
|
|
363
|
+
# Get the main formula text (excluding the label)
|
|
364
|
+
formula_text_parts = []
|
|
365
|
+
label_text = ""
|
|
366
|
+
|
|
367
|
+
for child in formula_element.children:
|
|
368
|
+
if hasattr(child, 'name') and child.name == "label":
|
|
369
|
+
# Extract equation label (e.g., "(1)", "(2)")
|
|
370
|
+
label_text = child.get_text().strip()
|
|
371
|
+
elif isinstance(child, NavigableString):
|
|
372
|
+
formula_text_parts.append(str(child))
|
|
373
|
+
else:
|
|
374
|
+
# Other elements within formula - get their text
|
|
375
|
+
formula_text_parts.append(child.get_text())
|
|
376
|
+
|
|
377
|
+
formula_text = "".join(formula_text_parts).strip()
|
|
378
|
+
|
|
379
|
+
if formula_text:
|
|
380
|
+
# Format as: *formula text* (label) if label exists
|
|
381
|
+
if label_text:
|
|
382
|
+
return f"*{formula_text}* {label_text}"
|
|
383
|
+
return f"*{formula_text}*"
|
|
384
|
+
return ""
|
|
385
|
+
|
|
339
386
|
def _table_to_markdown(self, table_element: Tag) -> str:
|
|
340
387
|
"""Convert a table element to simple markdown."""
|
|
341
388
|
markdown_lines = []
|
|
@@ -511,17 +558,25 @@ class TEI2MarkdownConverter:
|
|
|
511
558
|
unit = bibl_scope.get("unit", "").lower()
|
|
512
559
|
text = bibl_scope.get_text().strip()
|
|
513
560
|
|
|
514
|
-
if unit
|
|
561
|
+
if unit in ["vol", "volume"] and text:
|
|
515
562
|
bib_data['volume'] = text
|
|
516
563
|
elif unit == "issue" and text:
|
|
517
564
|
bib_data['issue'] = text
|
|
518
565
|
elif unit == "page" and text:
|
|
519
566
|
# Handle page ranges
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
567
|
+
from_val = bibl_scope.get("from")
|
|
568
|
+
to_val = bibl_scope.get("to")
|
|
569
|
+
if from_val and to_val:
|
|
570
|
+
# Both from and to in same element
|
|
571
|
+
bib_data['pages'] = f"{from_val}-{to_val}"
|
|
572
|
+
elif from_val:
|
|
573
|
+
# Only from specified, may get combined with another element
|
|
574
|
+
bib_data['pages'] = f"{from_val}-"
|
|
575
|
+
elif to_val and bib_data.get('pages'):
|
|
576
|
+
# Only to specified, append to existing from
|
|
577
|
+
bib_data['pages'] = bib_data['pages'] + to_val
|
|
578
|
+
elif text and not bib_data.get('pages'):
|
|
579
|
+
# Plain text, no from/to attributes
|
|
525
580
|
bib_data['pages'] = text
|
|
526
581
|
|
|
527
582
|
def _extract_author_info(self, author: Tag) -> dict:
|
|
@@ -593,8 +648,6 @@ class TEI2MarkdownConverter:
|
|
|
593
648
|
|
|
594
649
|
def _extract_year(self, date_text: str) -> str:
|
|
595
650
|
"""Extract year from date text, handling various formats."""
|
|
596
|
-
import re
|
|
597
|
-
|
|
598
651
|
# Look for 4-digit year patterns
|
|
599
652
|
year_match = re.search(r'\b(19|20)\d{2}\b', date_text)
|
|
600
653
|
if year_match:
|
|
@@ -629,6 +682,9 @@ class TEI2MarkdownConverter:
|
|
|
629
682
|
"""Build publication details string from extracted data."""
|
|
630
683
|
details = []
|
|
631
684
|
|
|
685
|
+
if ref_data.get('year'):
|
|
686
|
+
details.append(f"({ref_data['year']})")
|
|
687
|
+
|
|
632
688
|
if ref_data.get('volume'):
|
|
633
689
|
details.append(ref_data['volume'])
|
|
634
690
|
|
|
@@ -684,7 +740,6 @@ class TEI2MarkdownConverter:
|
|
|
684
740
|
raw_text = bibl_struct.get_text().strip()
|
|
685
741
|
|
|
686
742
|
# Remove reference number if present
|
|
687
|
-
import re
|
|
688
743
|
raw_text = re.sub(r'^\[\d+\]\s*', '', raw_text)
|
|
689
744
|
|
|
690
745
|
# Clean up excessive whitespace
|