opendataloader-pdf 0.0.0__py3-none-any.whl → 1.8.2__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.
- opendataloader_pdf/NOTICE.md +1 -1
- opendataloader_pdf/__init__.py +2 -2
- opendataloader_pdf/__main__.py +5 -0
- opendataloader_pdf/cli_options_generated.py +236 -0
- opendataloader_pdf/convert_generated.py +130 -0
- opendataloader_pdf/hybrid_server.py +273 -0
- opendataloader_pdf/jar/opendataloader-pdf-cli.jar +0 -0
- opendataloader_pdf/runner.py +71 -0
- opendataloader_pdf/wrapper.py +87 -96
- opendataloader_pdf-1.8.2.dist-info/METADATA +361 -0
- {opendataloader_pdf-0.0.0.dist-info → opendataloader_pdf-1.8.2.dist-info}/RECORD +13 -8
- {opendataloader_pdf-0.0.0.dist-info → opendataloader_pdf-1.8.2.dist-info}/WHEEL +1 -2
- opendataloader_pdf-1.8.2.dist-info/entry_points.txt +3 -0
- opendataloader_pdf-0.0.0.dist-info/METADATA +0 -91
- opendataloader_pdf-0.0.0.dist-info/top_level.txt +0 -1
opendataloader_pdf/NOTICE.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
Copyright © 2025 Hancom, Inc.
|
|
2
|
+
Copyright © 2025-2026 Hancom, Inc.
|
|
3
3
|
|
|
4
4
|
This Source Code Form is subject to the terms of the Mozilla Public License 2.0 (MPL-2.0).
|
|
5
5
|
If a copy of the MPL was not distributed with this file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
opendataloader_pdf/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .wrapper import run
|
|
1
|
+
from .wrapper import run, convert, run_jar
|
|
2
2
|
|
|
3
|
-
__all__ = ["run"]
|
|
3
|
+
__all__ = ["run", "convert", "run_jar"]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY
|
|
2
|
+
# Run `npm run generate-options` to regenerate
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
CLI option definitions for opendataloader-pdf.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Option metadata list
|
|
11
|
+
CLI_OPTIONS: List[Dict[str, Any]] = [
|
|
12
|
+
{
|
|
13
|
+
"name": "output-dir",
|
|
14
|
+
"python_name": "output_dir",
|
|
15
|
+
"short_name": "o",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": False,
|
|
18
|
+
"default": None,
|
|
19
|
+
"description": "Directory where output files are written. Default: input file directory",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "password",
|
|
23
|
+
"python_name": "password",
|
|
24
|
+
"short_name": "p",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"required": False,
|
|
27
|
+
"default": None,
|
|
28
|
+
"description": "Password for encrypted PDF files",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "format",
|
|
32
|
+
"python_name": "format",
|
|
33
|
+
"short_name": "f",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"required": False,
|
|
36
|
+
"default": None,
|
|
37
|
+
"description": "Output formats (comma-separated). Values: json, text, html, pdf, markdown, markdown-with-html, markdown-with-images. Default: json",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "quiet",
|
|
41
|
+
"python_name": "quiet",
|
|
42
|
+
"short_name": "q",
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"required": False,
|
|
45
|
+
"default": False,
|
|
46
|
+
"description": "Suppress console logging output",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "content-safety-off",
|
|
50
|
+
"python_name": "content_safety_off",
|
|
51
|
+
"short_name": None,
|
|
52
|
+
"type": "string",
|
|
53
|
+
"required": False,
|
|
54
|
+
"default": None,
|
|
55
|
+
"description": "Disable content safety filters. Values: all, hidden-text, off-page, tiny, hidden-ocg",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "keep-line-breaks",
|
|
59
|
+
"python_name": "keep_line_breaks",
|
|
60
|
+
"short_name": None,
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"required": False,
|
|
63
|
+
"default": False,
|
|
64
|
+
"description": "Preserve original line breaks in extracted text",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "replace-invalid-chars",
|
|
68
|
+
"python_name": "replace_invalid_chars",
|
|
69
|
+
"short_name": None,
|
|
70
|
+
"type": "string",
|
|
71
|
+
"required": False,
|
|
72
|
+
"default": " ",
|
|
73
|
+
"description": "Replacement character for invalid/unrecognized characters. Default: space",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "use-struct-tree",
|
|
77
|
+
"python_name": "use_struct_tree",
|
|
78
|
+
"short_name": None,
|
|
79
|
+
"type": "boolean",
|
|
80
|
+
"required": False,
|
|
81
|
+
"default": False,
|
|
82
|
+
"description": "Use PDF structure tree (tagged PDF) for reading order and semantic structure",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "table-method",
|
|
86
|
+
"python_name": "table_method",
|
|
87
|
+
"short_name": None,
|
|
88
|
+
"type": "string",
|
|
89
|
+
"required": False,
|
|
90
|
+
"default": "default",
|
|
91
|
+
"description": "Table detection method. Values: default (border-based), cluster (border + cluster). Default: default",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "reading-order",
|
|
95
|
+
"python_name": "reading_order",
|
|
96
|
+
"short_name": None,
|
|
97
|
+
"type": "string",
|
|
98
|
+
"required": False,
|
|
99
|
+
"default": "xycut",
|
|
100
|
+
"description": "Reading order algorithm. Values: off, xycut. Default: xycut",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "markdown-page-separator",
|
|
104
|
+
"python_name": "markdown_page_separator",
|
|
105
|
+
"short_name": None,
|
|
106
|
+
"type": "string",
|
|
107
|
+
"required": False,
|
|
108
|
+
"default": None,
|
|
109
|
+
"description": "Separator between pages in Markdown output. Use %%page-number%% for page numbers. Default: none",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "text-page-separator",
|
|
113
|
+
"python_name": "text_page_separator",
|
|
114
|
+
"short_name": None,
|
|
115
|
+
"type": "string",
|
|
116
|
+
"required": False,
|
|
117
|
+
"default": None,
|
|
118
|
+
"description": "Separator between pages in text output. Use %%page-number%% for page numbers. Default: none",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "html-page-separator",
|
|
122
|
+
"python_name": "html_page_separator",
|
|
123
|
+
"short_name": None,
|
|
124
|
+
"type": "string",
|
|
125
|
+
"required": False,
|
|
126
|
+
"default": None,
|
|
127
|
+
"description": "Separator between pages in HTML output. Use %%page-number%% for page numbers. Default: none",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "image-output",
|
|
131
|
+
"python_name": "image_output",
|
|
132
|
+
"short_name": None,
|
|
133
|
+
"type": "string",
|
|
134
|
+
"required": False,
|
|
135
|
+
"default": "external",
|
|
136
|
+
"description": "Image output mode. Values: off (no images), embedded (Base64 data URIs), external (file references). Default: external",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "image-format",
|
|
140
|
+
"python_name": "image_format",
|
|
141
|
+
"short_name": None,
|
|
142
|
+
"type": "string",
|
|
143
|
+
"required": False,
|
|
144
|
+
"default": "png",
|
|
145
|
+
"description": "Output format for extracted images. Values: png, jpeg. Default: png",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "image-dir",
|
|
149
|
+
"python_name": "image_dir",
|
|
150
|
+
"short_name": None,
|
|
151
|
+
"type": "string",
|
|
152
|
+
"required": False,
|
|
153
|
+
"default": None,
|
|
154
|
+
"description": "Directory for extracted images",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "pages",
|
|
158
|
+
"python_name": "pages",
|
|
159
|
+
"short_name": None,
|
|
160
|
+
"type": "string",
|
|
161
|
+
"required": False,
|
|
162
|
+
"default": None,
|
|
163
|
+
"description": "Pages to extract (e.g., \"1,3,5-7\"). Default: all pages",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "hybrid",
|
|
167
|
+
"python_name": "hybrid",
|
|
168
|
+
"short_name": None,
|
|
169
|
+
"type": "string",
|
|
170
|
+
"required": False,
|
|
171
|
+
"default": "off",
|
|
172
|
+
"description": "Hybrid backend for AI processing. Values: off (default), docling (docling-fast is deprecated alias)",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "hybrid-mode",
|
|
176
|
+
"python_name": "hybrid_mode",
|
|
177
|
+
"short_name": None,
|
|
178
|
+
"type": "string",
|
|
179
|
+
"required": False,
|
|
180
|
+
"default": "auto",
|
|
181
|
+
"description": "Hybrid triage mode. Values: auto (default, dynamic triage), full (skip triage, all pages to backend)",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "hybrid-ocr",
|
|
185
|
+
"python_name": "hybrid_ocr",
|
|
186
|
+
"short_name": None,
|
|
187
|
+
"type": "string",
|
|
188
|
+
"required": False,
|
|
189
|
+
"default": "auto",
|
|
190
|
+
"description": "Hybrid OCR mode for Docling backend. Values: auto (default, OCR only where needed), force (force full-page OCR)",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "hybrid-url",
|
|
194
|
+
"python_name": "hybrid_url",
|
|
195
|
+
"short_name": None,
|
|
196
|
+
"type": "string",
|
|
197
|
+
"required": False,
|
|
198
|
+
"default": None,
|
|
199
|
+
"description": "Hybrid backend server URL (overrides default)",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "hybrid-timeout",
|
|
203
|
+
"python_name": "hybrid_timeout",
|
|
204
|
+
"short_name": None,
|
|
205
|
+
"type": "string",
|
|
206
|
+
"required": False,
|
|
207
|
+
"default": "30000",
|
|
208
|
+
"description": "Hybrid backend request timeout in milliseconds. Default: 30000",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "hybrid-fallback",
|
|
212
|
+
"python_name": "hybrid_fallback",
|
|
213
|
+
"short_name": None,
|
|
214
|
+
"type": "boolean",
|
|
215
|
+
"required": False,
|
|
216
|
+
"default": True,
|
|
217
|
+
"description": "Fallback to Java processing on hybrid backend error. Default: true",
|
|
218
|
+
},
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def add_options_to_parser(parser) -> None:
|
|
223
|
+
"""Add all CLI options to an argparse.ArgumentParser."""
|
|
224
|
+
for opt in CLI_OPTIONS:
|
|
225
|
+
flags = []
|
|
226
|
+
if opt["short_name"]:
|
|
227
|
+
flags.append(f'-{opt["short_name"]}')
|
|
228
|
+
flags.append(f'--{opt["name"]}')
|
|
229
|
+
|
|
230
|
+
kwargs = {"help": opt["description"]}
|
|
231
|
+
if opt["type"] == "boolean":
|
|
232
|
+
kwargs["action"] = "store_true"
|
|
233
|
+
else:
|
|
234
|
+
kwargs["default"] = None
|
|
235
|
+
|
|
236
|
+
parser.add_argument(*flags, **kwargs)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY
|
|
2
|
+
# Run `npm run generate-options` to regenerate
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Auto-generated convert function for opendataloader-pdf.
|
|
6
|
+
"""
|
|
7
|
+
from typing import List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .runner import run_jar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def convert(
|
|
13
|
+
input_path: Union[str, List[str]],
|
|
14
|
+
output_dir: Optional[str] = None,
|
|
15
|
+
password: Optional[str] = None,
|
|
16
|
+
format: Optional[Union[str, List[str]]] = None,
|
|
17
|
+
quiet: bool = False,
|
|
18
|
+
content_safety_off: Optional[Union[str, List[str]]] = None,
|
|
19
|
+
keep_line_breaks: bool = False,
|
|
20
|
+
replace_invalid_chars: Optional[str] = None,
|
|
21
|
+
use_struct_tree: bool = False,
|
|
22
|
+
table_method: Optional[str] = None,
|
|
23
|
+
reading_order: Optional[str] = None,
|
|
24
|
+
markdown_page_separator: Optional[str] = None,
|
|
25
|
+
text_page_separator: Optional[str] = None,
|
|
26
|
+
html_page_separator: Optional[str] = None,
|
|
27
|
+
image_output: Optional[str] = None,
|
|
28
|
+
image_format: Optional[str] = None,
|
|
29
|
+
image_dir: Optional[str] = None,
|
|
30
|
+
pages: Optional[str] = None,
|
|
31
|
+
hybrid: Optional[str] = None,
|
|
32
|
+
hybrid_mode: Optional[str] = None,
|
|
33
|
+
hybrid_ocr: Optional[str] = None,
|
|
34
|
+
hybrid_url: Optional[str] = None,
|
|
35
|
+
hybrid_timeout: Optional[str] = None,
|
|
36
|
+
hybrid_fallback: bool = True,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Convert PDF(s) into the requested output format(s).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
input_path: One or more input PDF file paths or directories
|
|
43
|
+
output_dir: Directory where output files are written. Default: input file directory
|
|
44
|
+
password: Password for encrypted PDF files
|
|
45
|
+
format: Output formats (comma-separated). Values: json, text, html, pdf, markdown, markdown-with-html, markdown-with-images. Default: json
|
|
46
|
+
quiet: Suppress console logging output
|
|
47
|
+
content_safety_off: Disable content safety filters. Values: all, hidden-text, off-page, tiny, hidden-ocg
|
|
48
|
+
keep_line_breaks: Preserve original line breaks in extracted text
|
|
49
|
+
replace_invalid_chars: Replacement character for invalid/unrecognized characters. Default: space
|
|
50
|
+
use_struct_tree: Use PDF structure tree (tagged PDF) for reading order and semantic structure
|
|
51
|
+
table_method: Table detection method. Values: default (border-based), cluster (border + cluster). Default: default
|
|
52
|
+
reading_order: Reading order algorithm. Values: off, xycut. Default: xycut
|
|
53
|
+
markdown_page_separator: Separator between pages in Markdown output. Use %page-number% for page numbers. Default: none
|
|
54
|
+
text_page_separator: Separator between pages in text output. Use %page-number% for page numbers. Default: none
|
|
55
|
+
html_page_separator: Separator between pages in HTML output. Use %page-number% for page numbers. Default: none
|
|
56
|
+
image_output: Image output mode. Values: off (no images), embedded (Base64 data URIs), external (file references). Default: external
|
|
57
|
+
image_format: Output format for extracted images. Values: png, jpeg. Default: png
|
|
58
|
+
image_dir: Directory for extracted images
|
|
59
|
+
pages: Pages to extract (e.g., "1,3,5-7"). Default: all pages
|
|
60
|
+
hybrid: Hybrid backend for AI processing. Values: off (default), docling (docling-fast is deprecated alias)
|
|
61
|
+
hybrid_mode: Hybrid triage mode. Values: auto (default, dynamic triage), full (skip triage, all pages to backend)
|
|
62
|
+
hybrid_ocr: Hybrid OCR mode for Docling backend. Values: auto (default, OCR only where needed), force (force full-page OCR)
|
|
63
|
+
hybrid_url: Hybrid backend server URL (overrides default)
|
|
64
|
+
hybrid_timeout: Hybrid backend request timeout in milliseconds. Default: 30000
|
|
65
|
+
hybrid_fallback: Fallback to Java processing on hybrid backend error. Default: true
|
|
66
|
+
"""
|
|
67
|
+
args: List[str] = []
|
|
68
|
+
|
|
69
|
+
# Build input paths
|
|
70
|
+
if isinstance(input_path, list):
|
|
71
|
+
args.extend(input_path)
|
|
72
|
+
else:
|
|
73
|
+
args.append(input_path)
|
|
74
|
+
|
|
75
|
+
if output_dir:
|
|
76
|
+
args.extend(["--output-dir", output_dir])
|
|
77
|
+
if password:
|
|
78
|
+
args.extend(["--password", password])
|
|
79
|
+
if format:
|
|
80
|
+
if isinstance(format, list):
|
|
81
|
+
if format:
|
|
82
|
+
args.extend(["--format", ",".join(format)])
|
|
83
|
+
else:
|
|
84
|
+
args.extend(["--format", format])
|
|
85
|
+
if quiet:
|
|
86
|
+
args.append("--quiet")
|
|
87
|
+
if content_safety_off:
|
|
88
|
+
if isinstance(content_safety_off, list):
|
|
89
|
+
if content_safety_off:
|
|
90
|
+
args.extend(["--content-safety-off", ",".join(content_safety_off)])
|
|
91
|
+
else:
|
|
92
|
+
args.extend(["--content-safety-off", content_safety_off])
|
|
93
|
+
if keep_line_breaks:
|
|
94
|
+
args.append("--keep-line-breaks")
|
|
95
|
+
if replace_invalid_chars:
|
|
96
|
+
args.extend(["--replace-invalid-chars", replace_invalid_chars])
|
|
97
|
+
if use_struct_tree:
|
|
98
|
+
args.append("--use-struct-tree")
|
|
99
|
+
if table_method:
|
|
100
|
+
args.extend(["--table-method", table_method])
|
|
101
|
+
if reading_order:
|
|
102
|
+
args.extend(["--reading-order", reading_order])
|
|
103
|
+
if markdown_page_separator:
|
|
104
|
+
args.extend(["--markdown-page-separator", markdown_page_separator])
|
|
105
|
+
if text_page_separator:
|
|
106
|
+
args.extend(["--text-page-separator", text_page_separator])
|
|
107
|
+
if html_page_separator:
|
|
108
|
+
args.extend(["--html-page-separator", html_page_separator])
|
|
109
|
+
if image_output:
|
|
110
|
+
args.extend(["--image-output", image_output])
|
|
111
|
+
if image_format:
|
|
112
|
+
args.extend(["--image-format", image_format])
|
|
113
|
+
if image_dir:
|
|
114
|
+
args.extend(["--image-dir", image_dir])
|
|
115
|
+
if pages:
|
|
116
|
+
args.extend(["--pages", pages])
|
|
117
|
+
if hybrid:
|
|
118
|
+
args.extend(["--hybrid", hybrid])
|
|
119
|
+
if hybrid_mode:
|
|
120
|
+
args.extend(["--hybrid-mode", hybrid_mode])
|
|
121
|
+
if hybrid_ocr:
|
|
122
|
+
args.extend(["--hybrid-ocr", hybrid_ocr])
|
|
123
|
+
if hybrid_url:
|
|
124
|
+
args.extend(["--hybrid-url", hybrid_url])
|
|
125
|
+
if hybrid_timeout:
|
|
126
|
+
args.extend(["--hybrid-timeout", hybrid_timeout])
|
|
127
|
+
if hybrid_fallback:
|
|
128
|
+
args.append("--hybrid-fallback")
|
|
129
|
+
|
|
130
|
+
run_jar(args, quiet)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fast docling server using DocumentConverter singleton.
|
|
3
|
+
|
|
4
|
+
A lightweight FastAPI server optimized for hybrid PDF processing:
|
|
5
|
+
1. Using DocumentConverter singletons (no per-request initialization)
|
|
6
|
+
2. Returns only JSON (DoclingDocument format) - markdown/HTML generated by Java
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
opendataloader-pdf-hybrid [--port PORT] [--host HOST]
|
|
10
|
+
|
|
11
|
+
# Default: http://localhost:5002
|
|
12
|
+
opendataloader-pdf-hybrid
|
|
13
|
+
|
|
14
|
+
# Custom port
|
|
15
|
+
opendataloader-pdf-hybrid --port 5003
|
|
16
|
+
|
|
17
|
+
API Endpoints:
|
|
18
|
+
GET /health - Health check
|
|
19
|
+
POST /v1/convert/file - Convert PDF to JSON
|
|
20
|
+
|
|
21
|
+
The /v1/convert/file endpoint parameters:
|
|
22
|
+
- files: PDF file (multipart/form-data)
|
|
23
|
+
- page_ranges: Page range to process (optional)
|
|
24
|
+
- force_ocr: Force full-page OCR mode (optional, default: false)
|
|
25
|
+
|
|
26
|
+
Requirements:
|
|
27
|
+
Install with hybrid extra: pip install opendataloader-pdf[hybrid]
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
35
|
+
import traceback
|
|
36
|
+
from contextlib import asynccontextmanager
|
|
37
|
+
from typing import Optional
|
|
38
|
+
|
|
39
|
+
logging.basicConfig(
|
|
40
|
+
level=logging.INFO,
|
|
41
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
42
|
+
)
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# Configuration
|
|
46
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
47
|
+
DEFAULT_PORT = 5002
|
|
48
|
+
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB max file size
|
|
49
|
+
|
|
50
|
+
# Global converter instances (initialized on startup)
|
|
51
|
+
converter_auto = None # force_full_page_ocr=False
|
|
52
|
+
converter_force = None # force_full_page_ocr=True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _check_dependencies():
|
|
56
|
+
"""Check if hybrid dependencies are installed."""
|
|
57
|
+
missing = []
|
|
58
|
+
try:
|
|
59
|
+
import uvicorn # noqa: F401
|
|
60
|
+
except ImportError:
|
|
61
|
+
missing.append("uvicorn")
|
|
62
|
+
try:
|
|
63
|
+
import fastapi # noqa: F401
|
|
64
|
+
except ImportError:
|
|
65
|
+
missing.append("fastapi")
|
|
66
|
+
try:
|
|
67
|
+
import docling # noqa: F401
|
|
68
|
+
except ImportError:
|
|
69
|
+
missing.append("docling")
|
|
70
|
+
|
|
71
|
+
if missing:
|
|
72
|
+
raise ImportError(
|
|
73
|
+
f"Missing dependencies: {', '.join(missing)}. "
|
|
74
|
+
"Install with: pip install opendataloader-pdf[hybrid]"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_converter(force_full_page_ocr: bool = False):
|
|
79
|
+
"""Create a DocumentConverter with the specified OCR options.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
force_full_page_ocr: If True, force OCR on all pages regardless of text content.
|
|
83
|
+
If False (default), OCR only where needed.
|
|
84
|
+
"""
|
|
85
|
+
from docling.datamodel.base_models import InputFormat
|
|
86
|
+
from docling.datamodel.pipeline_options import (
|
|
87
|
+
EasyOcrOptions,
|
|
88
|
+
PdfPipelineOptions,
|
|
89
|
+
TableFormerMode,
|
|
90
|
+
TableStructureOptions,
|
|
91
|
+
)
|
|
92
|
+
from docling.document_converter import DocumentConverter, PdfFormatOption
|
|
93
|
+
|
|
94
|
+
pipeline_options = PdfPipelineOptions(
|
|
95
|
+
do_ocr=True,
|
|
96
|
+
do_table_structure=True,
|
|
97
|
+
ocr_options=EasyOcrOptions(force_full_page_ocr=force_full_page_ocr),
|
|
98
|
+
table_structure_options=TableStructureOptions(
|
|
99
|
+
mode=TableFormerMode.ACCURATE
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return DocumentConverter(
|
|
104
|
+
format_options={
|
|
105
|
+
InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def create_app():
|
|
111
|
+
"""Create and configure the FastAPI application."""
|
|
112
|
+
from fastapi import FastAPI, File, Form, UploadFile
|
|
113
|
+
from fastapi.responses import JSONResponse
|
|
114
|
+
|
|
115
|
+
@asynccontextmanager
|
|
116
|
+
async def lifespan(_app: FastAPI):
|
|
117
|
+
"""Lifespan context manager for startup and shutdown events."""
|
|
118
|
+
global converter_auto, converter_force
|
|
119
|
+
logger.info("Initializing DocumentConverters...")
|
|
120
|
+
start = time.perf_counter()
|
|
121
|
+
|
|
122
|
+
# Initialize both converters at startup
|
|
123
|
+
converter_auto = create_converter(force_full_page_ocr=False)
|
|
124
|
+
converter_force = create_converter(force_full_page_ocr=True)
|
|
125
|
+
|
|
126
|
+
elapsed = time.perf_counter() - start
|
|
127
|
+
logger.info(f"DocumentConverters initialized in {elapsed:.2f}s")
|
|
128
|
+
yield
|
|
129
|
+
# Cleanup on shutdown (if needed)
|
|
130
|
+
|
|
131
|
+
app = FastAPI(
|
|
132
|
+
title="Docling Fast Server",
|
|
133
|
+
description="Fast PDF conversion using docling SDK with singleton pattern",
|
|
134
|
+
version="1.0.0",
|
|
135
|
+
lifespan=lifespan,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@app.get("/health")
|
|
139
|
+
def health():
|
|
140
|
+
"""Health check endpoint."""
|
|
141
|
+
return {"status": "ok"}
|
|
142
|
+
|
|
143
|
+
@app.post("/v1/convert/file")
|
|
144
|
+
async def convert_file(
|
|
145
|
+
files: UploadFile = File(...),
|
|
146
|
+
page_ranges: Optional[str] = Form(default=None),
|
|
147
|
+
force_ocr: Optional[str] = Form(default=None),
|
|
148
|
+
):
|
|
149
|
+
"""Convert PDF file to JSON (DoclingDocument format).
|
|
150
|
+
|
|
151
|
+
Only JSON output is provided - markdown and HTML are generated by
|
|
152
|
+
Java processors for consistent reading order application.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
files: The PDF file to convert
|
|
156
|
+
page_ranges: Page range string "start-end" (e.g., "1-5") (optional)
|
|
157
|
+
force_ocr: If "true", use force full-page OCR mode (optional)
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
JSON response with document content.
|
|
161
|
+
"""
|
|
162
|
+
global converter_auto, converter_force
|
|
163
|
+
|
|
164
|
+
# Select converter based on force_ocr parameter
|
|
165
|
+
use_force_ocr = force_ocr and force_ocr.lower() == "true"
|
|
166
|
+
converter = converter_force if use_force_ocr else converter_auto
|
|
167
|
+
|
|
168
|
+
if converter is None:
|
|
169
|
+
return JSONResponse(
|
|
170
|
+
{"status": "failure", "errors": ["Server not initialized"]},
|
|
171
|
+
status_code=503,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Parse page_ranges string to tuple
|
|
175
|
+
page_range_tuple = None
|
|
176
|
+
if page_ranges:
|
|
177
|
+
try:
|
|
178
|
+
parts = page_ranges.split("-")
|
|
179
|
+
if len(parts) == 2:
|
|
180
|
+
page_range_tuple = (int(parts[0]), int(parts[1]))
|
|
181
|
+
except ValueError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Read and validate file size
|
|
185
|
+
content = await files.read()
|
|
186
|
+
if len(content) > MAX_FILE_SIZE:
|
|
187
|
+
return JSONResponse(
|
|
188
|
+
{
|
|
189
|
+
"status": "failure",
|
|
190
|
+
"errors": [f"File size exceeds maximum allowed ({MAX_FILE_SIZE // (1024*1024)}MB)"],
|
|
191
|
+
},
|
|
192
|
+
status_code=413,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Save uploaded file to temp location
|
|
196
|
+
tmp_path = None
|
|
197
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
|
198
|
+
tmp.write(content)
|
|
199
|
+
tmp_path = tmp.name
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
start = time.perf_counter()
|
|
203
|
+
if page_range_tuple:
|
|
204
|
+
result = converter.convert(tmp_path, page_range=page_range_tuple)
|
|
205
|
+
else:
|
|
206
|
+
result = converter.convert(tmp_path)
|
|
207
|
+
processing_time = time.perf_counter() - start
|
|
208
|
+
|
|
209
|
+
# Export to JSON (DoclingDocument format)
|
|
210
|
+
json_content = result.document.export_to_dict()
|
|
211
|
+
|
|
212
|
+
# Build response compatible with docling-serve format
|
|
213
|
+
response = {
|
|
214
|
+
"status": "success",
|
|
215
|
+
"document": {
|
|
216
|
+
"json_content": json_content,
|
|
217
|
+
},
|
|
218
|
+
"processing_time": processing_time,
|
|
219
|
+
"force_ocr": use_force_ocr,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return JSONResponse(response)
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"PDF conversion failed: {e}\n{traceback.format_exc()}")
|
|
226
|
+
return JSONResponse(
|
|
227
|
+
{"status": "failure", "errors": ["PDF conversion failed"]},
|
|
228
|
+
status_code=500,
|
|
229
|
+
)
|
|
230
|
+
finally:
|
|
231
|
+
if tmp_path and os.path.exists(tmp_path):
|
|
232
|
+
os.unlink(tmp_path)
|
|
233
|
+
|
|
234
|
+
return app
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def main():
|
|
238
|
+
"""Run the server."""
|
|
239
|
+
_check_dependencies()
|
|
240
|
+
import uvicorn
|
|
241
|
+
|
|
242
|
+
parser = argparse.ArgumentParser(description="Docling Fast Server for opendataloader-pdf")
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"--host",
|
|
245
|
+
default=DEFAULT_HOST,
|
|
246
|
+
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--port",
|
|
250
|
+
type=int,
|
|
251
|
+
default=DEFAULT_PORT,
|
|
252
|
+
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--log-level",
|
|
256
|
+
default="info",
|
|
257
|
+
choices=["debug", "info", "warning", "error"],
|
|
258
|
+
help="Log level (default: info)",
|
|
259
|
+
)
|
|
260
|
+
args = parser.parse_args()
|
|
261
|
+
|
|
262
|
+
logger.info(f"Starting Docling Fast Server on http://{args.host}:{args.port}")
|
|
263
|
+
app = create_app()
|
|
264
|
+
uvicorn.run(
|
|
265
|
+
app,
|
|
266
|
+
host=args.host,
|
|
267
|
+
port=args.port,
|
|
268
|
+
log_level=args.log_level,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
main()
|
|
Binary file
|