grobid-client-python 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
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/format/TEI2LossyJSON.py +80 -26
- grobid_client/format/TEI2LossyJSON_cli.py +131 -0
- grobid_client/format/TEI2Markdown.py +82 -27
- grobid_client/format/TEI2Markdown_cli.py +130 -0
- grobid_client/format/__main__.py +48 -0
- grobid_client/grobid_client.py +21 -1
- {grobid_client_python-0.1.2.dist-info → grobid_client_python-0.1.4.dist-info}/METADATA +28 -1
- grobid_client_python-0.1.4.dist-info/RECORD +15 -0
- grobid_client_python-0.1.2.dist-info/RECORD +0 -12
- {grobid_client_python-0.1.2.dist-info → grobid_client_python-0.1.4.dist-info}/WHEEL +0 -0
- {grobid_client_python-0.1.2.dist-info → grobid_client_python-0.1.4.dist-info}/entry_points.txt +0 -0
- {grobid_client_python-0.1.2.dist-info → grobid_client_python-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
|
@@ -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
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Standalone CLI for TEI2Markdown converter.
|
|
4
|
+
|
|
5
|
+
This script provides a command-line interface for converting TEI XML files to Markdown format
|
|
6
|
+
using the TEI2MarkdownConverter.
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .TEI2Markdown import TEI2MarkdownConverter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def setup_logging(verbose: bool = False):
|
|
17
|
+
"""Setup logging configuration."""
|
|
18
|
+
level = logging.INFO if verbose else logging.WARNING
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=level,
|
|
21
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
22
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def convert_single_file(input_file: Path, output_file: Path, verbose: bool = False) -> bool:
|
|
27
|
+
"""Convert a single TEI file to Markdown format."""
|
|
28
|
+
try:
|
|
29
|
+
if verbose:
|
|
30
|
+
logging.info(f"Converting {input_file} to {output_file}")
|
|
31
|
+
|
|
32
|
+
converter = TEI2MarkdownConverter()
|
|
33
|
+
result = converter.convert_tei_file(input_file)
|
|
34
|
+
|
|
35
|
+
if result is None:
|
|
36
|
+
logging.error(f"Failed to convert {input_file}: TEI file is not well-formed or empty")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# Ensure output directory exists
|
|
40
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Write Markdown output
|
|
43
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
44
|
+
f.write(result)
|
|
45
|
+
|
|
46
|
+
if verbose:
|
|
47
|
+
logging.info(f"Successfully converted {input_file} to {output_file}")
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logging.error(f"Error converting {input_file}: {str(e)}")
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
"""Main CLI entry point."""
|
|
58
|
+
parser = argparse.ArgumentParser(
|
|
59
|
+
description="Convert TEI XML files to Markdown format using TEI2Markdown converter",
|
|
60
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
61
|
+
epilog="""
|
|
62
|
+
Examples:
|
|
63
|
+
# Convert a single TEI file
|
|
64
|
+
python -m grobid_client.format.TEI2Markdown --input input.tei.xml --output output.md
|
|
65
|
+
|
|
66
|
+
# Convert with verbose logging
|
|
67
|
+
python -m grobid_client.format.TEI2Markdown --input input.tei.xml --output output.md --verbose
|
|
68
|
+
|
|
69
|
+
# Convert and output to stdout
|
|
70
|
+
python -m grobid_client.format.TEI2Markdown --input input.tei.xml
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--input", "-i",
|
|
76
|
+
type=Path,
|
|
77
|
+
required=True,
|
|
78
|
+
help="Input TEI XML file to convert"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--output", "-o",
|
|
83
|
+
type=Path,
|
|
84
|
+
help="Output Markdown file (if not specified, prints to stdout)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--verbose", "-v",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help="Enable verbose logging"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
args = parser.parse_args()
|
|
94
|
+
|
|
95
|
+
# Setup logging
|
|
96
|
+
setup_logging(args.verbose)
|
|
97
|
+
|
|
98
|
+
# Validate input file
|
|
99
|
+
if not args.input.exists():
|
|
100
|
+
logging.error(f"Input file does not exist: {args.input}")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
if not args.input.is_file():
|
|
104
|
+
logging.error(f"Input path is not a file: {args.input}")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
# Convert the file
|
|
108
|
+
if args.output:
|
|
109
|
+
success = convert_single_file(args.input, args.output, args.verbose)
|
|
110
|
+
sys.exit(0 if success else 1)
|
|
111
|
+
else:
|
|
112
|
+
# Output to stdout
|
|
113
|
+
try:
|
|
114
|
+
converter = TEI2MarkdownConverter()
|
|
115
|
+
result = converter.convert_tei_file(args.input)
|
|
116
|
+
|
|
117
|
+
if result is None:
|
|
118
|
+
logging.error(f"Failed to convert {args.input}: TEI file is not well-formed or empty")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
# Print Markdown to stdout
|
|
122
|
+
print(result)
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logging.error(f"Error converting {args.input}: {str(e)}")
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package entry point for format converters.
|
|
3
|
+
|
|
4
|
+
This provides a menu to choose between TEI2LossyJSON and TEI2Markdown converters.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
"""Main entry point that provides a menu for converter selection."""
|
|
12
|
+
|
|
13
|
+
# Check if a converter was specified
|
|
14
|
+
if len(sys.argv) < 2:
|
|
15
|
+
print("GROBID format converters - Choose a converter to run")
|
|
16
|
+
print("\nUsage:")
|
|
17
|
+
print(" python -m grobid_client.format <converter> [options]")
|
|
18
|
+
print("\nAvailable converters:")
|
|
19
|
+
print(" TEI2LossyJSON - Convert TEI XML to JSON format")
|
|
20
|
+
print(" TEI2Markdown - Convert TEI XML to Markdown format")
|
|
21
|
+
print("\nExamples:")
|
|
22
|
+
print(" python -m grobid_client.format TEI2LossyJSON --input file.tei.xml --output output.json")
|
|
23
|
+
print(" python -m grobid_client.format TEI2Markdown --input file.tei.xml --output output.md")
|
|
24
|
+
print("\nGet help for specific converter:")
|
|
25
|
+
print(" python -m grobid_client.format TEI2LossyJSON --help")
|
|
26
|
+
print(" python -m grobid_client.format TEI2Markdown --help")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
converter = sys.argv[1]
|
|
30
|
+
|
|
31
|
+
if converter == "TEI2LossyJSON":
|
|
32
|
+
from .TEI2LossyJSON_cli import main as lossy_main
|
|
33
|
+
# Replace sys.argv to pass remaining args to the converter
|
|
34
|
+
sys.argv = ["TEI2LossyJSON"] + sys.argv[2:]
|
|
35
|
+
lossy_main()
|
|
36
|
+
elif converter == "TEI2Markdown":
|
|
37
|
+
from .TEI2Markdown_cli import main as markdown_main
|
|
38
|
+
# Replace sys.argv to pass remaining args to the converter
|
|
39
|
+
sys.argv = ["TEI2Markdown"] + sys.argv[2:]
|
|
40
|
+
markdown_main()
|
|
41
|
+
else:
|
|
42
|
+
print(f"Unknown converter: {converter}")
|
|
43
|
+
print("Available converters: TEI2LossyJSON, TEI2Markdown")
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
grobid_client/grobid_client.py
CHANGED
|
@@ -342,6 +342,7 @@ class GrobidClient(ApiClient):
|
|
|
342
342
|
json_output=False,
|
|
343
343
|
markdown_output=False
|
|
344
344
|
):
|
|
345
|
+
start_time = time.time()
|
|
345
346
|
batch_size_pdf = self.config["batch_size"]
|
|
346
347
|
|
|
347
348
|
# First pass: count all eligible files
|
|
@@ -433,11 +434,19 @@ class GrobidClient(ApiClient):
|
|
|
433
434
|
errors_files_count += batch_errors
|
|
434
435
|
skipped_files_count += batch_skipped
|
|
435
436
|
|
|
437
|
+
runtime = time.time() - start_time
|
|
438
|
+
docs_per_second = processed_files_count / runtime if runtime > 0 else 0
|
|
439
|
+
seconds_per_doc = runtime / processed_files_count if processed_files_count > 0 else 0
|
|
440
|
+
|
|
436
441
|
# Log final statistics - always visible
|
|
437
442
|
print(f"Processing completed: {processed_files_count} out of {total_files} files processed")
|
|
438
443
|
print(f"Errors: {errors_files_count} out of {total_files} files processed")
|
|
439
444
|
if skipped_files_count > 0:
|
|
440
445
|
print(f"Skipped: {skipped_files_count} out of {total_files} files (already existed, use --force to reprocess)")
|
|
446
|
+
|
|
447
|
+
print(f"⏱️ Total runtime: {runtime:.2f} seconds")
|
|
448
|
+
print(f"🚀 Speed: {docs_per_second:.2f} documents/second")
|
|
449
|
+
print(f" Throughput: {seconds_per_doc:.2f} seconds/document")
|
|
441
450
|
|
|
442
451
|
def process_batch(
|
|
443
452
|
self,
|
|
@@ -459,6 +468,7 @@ class GrobidClient(ApiClient):
|
|
|
459
468
|
json_output=False,
|
|
460
469
|
markdown_output=False
|
|
461
470
|
):
|
|
471
|
+
batch_start_time = time.time()
|
|
462
472
|
if verbose:
|
|
463
473
|
self.logger.info(f"{len(input_files)} files to process in current batch")
|
|
464
474
|
|
|
@@ -598,7 +608,7 @@ class GrobidClient(ApiClient):
|
|
|
598
608
|
converter = TEI2MarkdownConverter()
|
|
599
609
|
markdown_data = converter.convert_tei_file(filename)
|
|
600
610
|
|
|
601
|
-
if markdown_data:
|
|
611
|
+
if markdown_data is not None:
|
|
602
612
|
markdown_filename = filename.replace('.grobid.tei.xml', '.md')
|
|
603
613
|
# Always write Markdown file when TEI is written (respects --force behavior)
|
|
604
614
|
markdown_filename_expanded = os.path.expanduser(markdown_filename)
|
|
@@ -613,6 +623,16 @@ class GrobidClient(ApiClient):
|
|
|
613
623
|
except OSError as e:
|
|
614
624
|
self.logger.error(f"Failed to write TEI XML file {filename}: {str(e)}")
|
|
615
625
|
|
|
626
|
+
# Calculate batch statistics
|
|
627
|
+
batch_runtime = time.time() - batch_start_time
|
|
628
|
+
batch_docs_per_second = processed_count / batch_runtime if batch_runtime > 0 else 0
|
|
629
|
+
batch_seconds_per_docs = batch_runtime / processed_count if processed_count > 0 else 0
|
|
630
|
+
|
|
631
|
+
if verbose:
|
|
632
|
+
self.logger.info(f"⏱️ Runtime: {batch_runtime:.2f} seconds")
|
|
633
|
+
self.logger.info(f"🚀 Speed: {batch_docs_per_second:.2f} documents/second")
|
|
634
|
+
self.logger.info(f" Throughput: {batch_seconds_per_docs:.2f} seconds/document")
|
|
635
|
+
|
|
616
636
|
return processed_count, error_count, skipped_count
|
|
617
637
|
|
|
618
638
|
def process_pdf(
|
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
grobid_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
grobid_client/client.py,sha256=crvUiVnnkn6tFwO4lKQg2cTxAHZxCBAmeNSVtuL4ZA0,6009
|
|
3
|
+
grobid_client/grobid_client.py,sha256=uXmDwbgEQosKwJXPcNu1aIIdqCxeanmr8RqtSeghIgk,40547
|
|
4
|
+
grobid_client/format/TEI2LossyJSON.py,sha256=1Ew2Y3zbLtWfz2LEyqAVRPu66850Nney9JHjwoTyI3Y,49162
|
|
5
|
+
grobid_client/format/TEI2LossyJSON_cli.py,sha256=6l5nNVc7m03TGI19eIqhN6cxYMH5mZT4K9aK1P0g3So,3796
|
|
6
|
+
grobid_client/format/TEI2Markdown.py,sha256=7HTLuuy8OJFhA58BZ1cxKKF4GGIXIWuxTMRd1tg9PdQ,29505
|
|
7
|
+
grobid_client/format/TEI2Markdown_cli.py,sha256=wVaMIXfDQUW8Fqu9b5Gj0Q-MbZ2Gv1jCRxgOoQk5bHA,3689
|
|
8
|
+
grobid_client/format/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
grobid_client/format/__main__.py,sha256=jBiiNtyhn7Ys6RC6ynUD4cv1YWXHmpZw4rHmnYfVybI,1804
|
|
10
|
+
grobid_client/format/validate_json_refs.py,sha256=_ieADmrHD77EFwblTNH1_i0x3P7SCmsaTi-PZ6SI79M,14842
|
|
11
|
+
grobid_client_python-0.1.4.dist-info/METADATA,sha256=GRs12gLkqakZJ1YXO0Ils_DLLf9k5wSSkSX-ZkCbwtk,34414
|
|
12
|
+
grobid_client_python-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
grobid_client_python-0.1.4.dist-info/entry_points.txt,sha256=-WwPi8D8eXEbDB6QvitvHjxSGBeyLCyFvAoZcaDGswQ,67
|
|
14
|
+
grobid_client_python-0.1.4.dist-info/top_level.txt,sha256=HnjgWxdJNKTLIN86XkgRAfVeEK4C71fc9X4fe7qdMvA,14
|
|
15
|
+
grobid_client_python-0.1.4.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
grobid_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
grobid_client/client.py,sha256=crvUiVnnkn6tFwO4lKQg2cTxAHZxCBAmeNSVtuL4ZA0,6009
|
|
3
|
-
grobid_client/grobid_client.py,sha256=FQ-g6YjgcA6s63aOXO2MPsttFpo9x8UVD9WNPRn96OU,39465
|
|
4
|
-
grobid_client/format/TEI2LossyJSON.py,sha256=jbWBiGkCMZxNNBudn2C6vPWX-qQEPi8JjesOPOYVn_g,46048
|
|
5
|
-
grobid_client/format/TEI2Markdown.py,sha256=0NIe0aSCiq7NTDEWwPbKHhsRSudVGXWJ-vjMG9_WTNY,26779
|
|
6
|
-
grobid_client/format/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
grobid_client/format/validate_json_refs.py,sha256=_ieADmrHD77EFwblTNH1_i0x3P7SCmsaTi-PZ6SI79M,14842
|
|
8
|
-
grobid_client_python-0.1.2.dist-info/METADATA,sha256=8VZWtAFYPK70kbjoP75CJVM-lxQQTha4NSCvpC6De4k,33620
|
|
9
|
-
grobid_client_python-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
grobid_client_python-0.1.2.dist-info/entry_points.txt,sha256=-WwPi8D8eXEbDB6QvitvHjxSGBeyLCyFvAoZcaDGswQ,67
|
|
11
|
-
grobid_client_python-0.1.2.dist-info/top_level.txt,sha256=HnjgWxdJNKTLIN86XkgRAfVeEK4C71fc9X4fe7qdMvA,14
|
|
12
|
-
grobid_client_python-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
{grobid_client_python-0.1.2.dist-info → grobid_client_python-0.1.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|