BabelDOC 0.1.26__tar.gz → 0.1.27__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.
- {babeldoc-0.1.26 → babeldoc-0.1.27}/PKG-INFO +3 -2
- {babeldoc-0.1.26 → babeldoc-0.1.27}/README.md +2 -1
- babeldoc-0.1.27/babeldoc/__init__.py +1 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/const.py +1 -1
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/il_translator.py +2 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/layout_parser.py +33 -61
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/paragraph_finder.py +2 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/translator/translator.py +50 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/docvision/doclayout.py +54 -12
- babeldoc-0.1.27/babeldoc/docvision/rpc_doclayout.py +304 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/main.py +35 -8
- {babeldoc-0.1.26 → babeldoc-0.1.27}/pyproject.toml +2 -2
- {babeldoc-0.1.26 → babeldoc-0.1.27}/uv.lock +1 -1
- babeldoc-0.1.26/babeldoc/__init__.py +0 -1
- babeldoc-0.1.26/babeldoc/docvision/rpc_doclayout.py +0 -176
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.cursorignore +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/dependabot.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/labels.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/release-drafter.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/workflows/codeql.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/workflows/docs.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/workflows/labeler.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/workflows/publish-to-pypi.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.github/workflows/test.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.gitignore +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/.pre-commit-config.yaml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/LICENSE +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/assets/assets.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/assets/embedding_assets_metadata.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/asynchronize/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/converter.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/backend/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/backend/pdf_creater.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/frontend/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/frontend/il_creater.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/il_version_1.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/il_version_1.rnc +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/il_version_1.rng +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/il_version_1.xsd +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/add_debug_information.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/remove_descent.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/styles_and_formulas.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/midend/typesetting.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/translator/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/translator/cache.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/utils/fontmap.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/utils/layout_helper.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/utils/style_helper.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/xml_converter.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/docvision/README.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/docvision/__init__.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/high_level.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/pdfinterp.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/progress_monitor.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/tools/generate_font_metadata.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/translation_config.py +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/CODE_OF_CONDUCT.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/CONTRIBUTING.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/CONTRIBUTOR_REWARD.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/README.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/README.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/deploy.sh +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-banner.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-contributor_reward_example.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-preview.gif +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-small-logo.png +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/index.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/intro-to-pdf-object.md +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/docs/requirements.txt +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/mkdocs.yml +0 -0
- {babeldoc-0.1.26 → babeldoc-0.1.27}/tests/test_translation_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BabelDOC
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Summary: Yet Another Document Translator
|
|
5
5
|
Project-URL: Homepage, https://github.com/funstory-ai/BabelDOC
|
|
6
6
|
Project-URL: Issues, https://github.com/funstory-ai/BabelDOC/issues
|
|
@@ -65,7 +65,8 @@ Description-Content-Type: text/markdown
|
|
|
65
65
|
|
|
66
66
|
PDF scientific paper translation and bilingual comparison library.
|
|
67
67
|
|
|
68
|
-
- Beta version launched [Immersive Translate - BabelDOC](https://app.immersivetranslate.com/babel-doc/) 1000 free pages per month.
|
|
68
|
+
- **Online Service**: Beta version launched [Immersive Translate - BabelDOC](https://app.immersivetranslate.com/babel-doc/) 1000 free pages per month.
|
|
69
|
+
- **Self-deployment**: [PDFMathTranslate](https://github.com/Byaidu/PDFMathTranslate) 1.9.3+ Experimental support for BabelDOC, available for self-deployment + WebUI with more translation services.
|
|
69
70
|
- Provides a simple [command line interface](#getting-started).
|
|
70
71
|
- Provides a [Python API](#python-api).
|
|
71
72
|
- Mainly designed to be embedded into other programs, but can also be used directly for simple translation tasks.
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
|
|
31
31
|
PDF scientific paper translation and bilingual comparison library.
|
|
32
32
|
|
|
33
|
-
- Beta version launched [Immersive Translate - BabelDOC](https://app.immersivetranslate.com/babel-doc/) 1000 free pages per month.
|
|
33
|
+
- **Online Service**: Beta version launched [Immersive Translate - BabelDOC](https://app.immersivetranslate.com/babel-doc/) 1000 free pages per month.
|
|
34
|
+
- **Self-deployment**: [PDFMathTranslate](https://github.com/Byaidu/PDFMathTranslate) 1.9.3+ Experimental support for BabelDOC, available for self-deployment + WebUI with more translation services.
|
|
34
35
|
- Provides a simple [command line interface](#getting-started).
|
|
35
36
|
- Provides a [Python API](#python-api).
|
|
36
37
|
- Mainly designed to be embedded into other programs, but can also be used directly for simple translation tasks.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.27"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import concurrent.futures
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from tqdm import tqdm
|
|
@@ -555,6 +556,7 @@ class ILTranslator:
|
|
|
555
556
|
return
|
|
556
557
|
|
|
557
558
|
translated_text = self.translate_engine.translate(text)
|
|
559
|
+
translated_text = re.sub(r"[. 。…]{20,}", ".", translated_text)
|
|
558
560
|
|
|
559
561
|
tracker.set_output(translated_text)
|
|
560
562
|
|
|
@@ -110,71 +110,43 @@ class LayoutParser:
|
|
|
110
110
|
def process(self, docs: il_version_1.Document, mupdf_doc: Document):
|
|
111
111
|
"""Generate layouts for all pages that need to be translated."""
|
|
112
112
|
# Get pages that need to be translated
|
|
113
|
-
|
|
114
|
-
page
|
|
115
|
-
for page in docs.page
|
|
116
|
-
if self.translation_config.should_translate_page(page.page_number + 1)
|
|
117
|
-
]
|
|
118
|
-
total = len(pages_to_translate)
|
|
113
|
+
total = len(docs.page)
|
|
119
114
|
with self.translation_config.progress_monitor.stage_start(
|
|
120
115
|
self.stage_name,
|
|
121
116
|
total,
|
|
122
117
|
) as progress:
|
|
123
|
-
# Process
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
pix = mupdf_doc[page.page_number].get_pixmap(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# Get predictions for the batch
|
|
141
|
-
layouts_batch = self.model.predict(batch_images, batch_size=batch_size)
|
|
142
|
-
|
|
143
|
-
# Process predictions for each page
|
|
144
|
-
for page, layouts in zip(batch_pages, layouts_batch, strict=False):
|
|
145
|
-
page_layouts = []
|
|
146
|
-
self._save_debug_image(
|
|
147
|
-
batch_images[batch_pages.index(page)],
|
|
148
|
-
layouts,
|
|
149
|
-
page.page_number + 1,
|
|
118
|
+
# Process predictions for each page
|
|
119
|
+
for page, layouts in self.model.handle_document(
|
|
120
|
+
docs.page, mupdf_doc, self.translation_config, self._save_debug_image
|
|
121
|
+
):
|
|
122
|
+
page_layouts = []
|
|
123
|
+
for layout in layouts.boxes:
|
|
124
|
+
# Convert coordinate system from picture to il
|
|
125
|
+
# system to the il coordinate system
|
|
126
|
+
x0, y0, x1, y1 = layout.xyxy
|
|
127
|
+
pix = mupdf_doc[page.page_number].get_pixmap()
|
|
128
|
+
h, w = pix.height, pix.width
|
|
129
|
+
x0, y0, x1, y1 = (
|
|
130
|
+
np.clip(int(x0 - 1), 0, w - 1),
|
|
131
|
+
np.clip(int(h - y1 - 1), 0, h - 1),
|
|
132
|
+
np.clip(int(x1 + 1), 0, w - 1),
|
|
133
|
+
np.clip(int(h - y0 + 1), 0, h - 1),
|
|
150
134
|
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
page_layout = il_version_1.PageLayout(
|
|
164
|
-
id=len(page_layouts) + 1,
|
|
165
|
-
box=il_version_1.Box(
|
|
166
|
-
x0.item(),
|
|
167
|
-
y0.item(),
|
|
168
|
-
x1.item(),
|
|
169
|
-
y1.item(),
|
|
170
|
-
),
|
|
171
|
-
conf=layout.conf.item(),
|
|
172
|
-
class_name=layouts.names[layout.cls],
|
|
173
|
-
)
|
|
174
|
-
page_layouts.append(page_layout)
|
|
135
|
+
page_layout = il_version_1.PageLayout(
|
|
136
|
+
id=len(page_layouts) + 1,
|
|
137
|
+
box=il_version_1.Box(
|
|
138
|
+
x0.item(),
|
|
139
|
+
y0.item(),
|
|
140
|
+
x1.item(),
|
|
141
|
+
y1.item(),
|
|
142
|
+
),
|
|
143
|
+
conf=layout.conf.item(),
|
|
144
|
+
class_name=layouts.names[layout.cls],
|
|
145
|
+
)
|
|
146
|
+
page_layouts.append(page_layout)
|
|
175
147
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
148
|
+
page.page_layout = page_layouts
|
|
149
|
+
self._save_debug_box_to_page(page)
|
|
150
|
+
progress.advance(1)
|
|
179
151
|
|
|
180
|
-
|
|
152
|
+
return docs
|
|
@@ -244,6 +244,7 @@ class ParagraphFinder:
|
|
|
244
244
|
def is_text_layout(self, layout: Layout):
|
|
245
245
|
return layout is not None and layout.name in [
|
|
246
246
|
"plain text",
|
|
247
|
+
"tiny text",
|
|
247
248
|
"title",
|
|
248
249
|
"abandon",
|
|
249
250
|
"figure_caption",
|
|
@@ -312,6 +313,7 @@ class ParagraphFinder:
|
|
|
312
313
|
"figure_caption",
|
|
313
314
|
"abandon",
|
|
314
315
|
"plain text",
|
|
316
|
+
"tiny text",
|
|
315
317
|
"title",
|
|
316
318
|
]
|
|
317
319
|
char_box = char.box
|
|
@@ -8,6 +8,7 @@ import unicodedata
|
|
|
8
8
|
from abc import ABC
|
|
9
9
|
from abc import abstractmethod
|
|
10
10
|
|
|
11
|
+
import httpx
|
|
11
12
|
import openai
|
|
12
13
|
import requests
|
|
13
14
|
|
|
@@ -271,3 +272,52 @@ class OpenAITranslator(BaseTranslator):
|
|
|
271
272
|
|
|
272
273
|
def get_rich_text_right_placeholder(self, placeholder_id: int):
|
|
273
274
|
return self.get_formular_placeholder(placeholder_id + 1)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TranslateTranslator(BaseTranslator):
|
|
278
|
+
# https://github.com/openai/openai-python
|
|
279
|
+
name = "openai"
|
|
280
|
+
|
|
281
|
+
def __init__(
|
|
282
|
+
self,
|
|
283
|
+
lang_in,
|
|
284
|
+
lang_out,
|
|
285
|
+
url=None,
|
|
286
|
+
ignore_cache=False,
|
|
287
|
+
):
|
|
288
|
+
super().__init__(lang_in, lang_out, ignore_cache)
|
|
289
|
+
self.client = httpx.Client()
|
|
290
|
+
self.url = url
|
|
291
|
+
|
|
292
|
+
def do_translate(self, text) -> str:
|
|
293
|
+
response = self.client.post(
|
|
294
|
+
self.url,
|
|
295
|
+
json={
|
|
296
|
+
"text": [text],
|
|
297
|
+
"src": "Englih",
|
|
298
|
+
"tgt": "Simplifed Chinese",
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
return response.json()["text"][0]
|
|
302
|
+
|
|
303
|
+
def prompt(self, text):
|
|
304
|
+
return [
|
|
305
|
+
{
|
|
306
|
+
"role": "system",
|
|
307
|
+
"content": "You are a professional,authentic machine translation engine.",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"role": "user",
|
|
311
|
+
"content": f";; Treat next line as plain text input and translate it into {self.lang_out}, output translation ONLY. If translation is unnecessary (e.g. proper nouns, codes, {'{{1}}, etc. '}), return the original text. NO explanations. NO notes. Input:\n\n{text}",
|
|
312
|
+
},
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
def get_formular_placeholder(self, placeholder_id: int):
|
|
316
|
+
return "{{v" + str(placeholder_id) + "}}"
|
|
317
|
+
return "{{" + str(placeholder_id) + "}}"
|
|
318
|
+
|
|
319
|
+
def get_rich_text_left_placeholder(self, placeholder_id: int):
|
|
320
|
+
return self.get_formular_placeholder(placeholder_id)
|
|
321
|
+
|
|
322
|
+
def get_rich_text_right_placeholder(self, placeholder_id: int):
|
|
323
|
+
return self.get_formular_placeholder(placeholder_id + 1)
|
|
@@ -2,12 +2,15 @@ import abc
|
|
|
2
2
|
import ast
|
|
3
3
|
import logging
|
|
4
4
|
import platform
|
|
5
|
+
from collections.abc import Generator
|
|
5
6
|
|
|
6
7
|
import cv2
|
|
7
8
|
import numpy as np
|
|
8
9
|
import onnx
|
|
9
10
|
import onnxruntime
|
|
11
|
+
import pymupdf
|
|
10
12
|
|
|
13
|
+
import babeldoc.document_il.il_version_1
|
|
11
14
|
from babeldoc.assets.assets import get_doclayout_onnx_model_path
|
|
12
15
|
|
|
13
16
|
# from huggingface_hub import hf_hub_download
|
|
@@ -15,6 +18,19 @@ from babeldoc.assets.assets import get_doclayout_onnx_model_path
|
|
|
15
18
|
logger = logging.getLogger(__name__)
|
|
16
19
|
|
|
17
20
|
|
|
21
|
+
class YoloResult:
|
|
22
|
+
"""Helper class to store detection results from ONNX model."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, names, boxes=None, boxes_data=None):
|
|
25
|
+
if boxes is not None:
|
|
26
|
+
self.boxes = boxes
|
|
27
|
+
else:
|
|
28
|
+
assert boxes_data is not None
|
|
29
|
+
self.boxes = [YoloBox(data=d) for d in boxes_data]
|
|
30
|
+
self.boxes.sort(key=lambda x: x.conf, reverse=True)
|
|
31
|
+
self.names = names
|
|
32
|
+
|
|
33
|
+
|
|
18
34
|
class DocLayoutModel(abc.ABC):
|
|
19
35
|
@staticmethod
|
|
20
36
|
def load_onnx():
|
|
@@ -42,18 +58,19 @@ class DocLayoutModel(abc.ABC):
|
|
|
42
58
|
**kwargs: Additional arguments.
|
|
43
59
|
"""
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
@abc.abstractmethod
|
|
62
|
+
def handle_document(
|
|
63
|
+
self,
|
|
64
|
+
pages: list[babeldoc.document_il.il_version_1.Page],
|
|
65
|
+
mupdf_doc: pymupdf.Document,
|
|
66
|
+
translate_config,
|
|
67
|
+
save_debug_image,
|
|
68
|
+
) -> Generator[
|
|
69
|
+
tuple[babeldoc.document_il.il_version_1.Page, YoloResult], None, None
|
|
70
|
+
]:
|
|
71
|
+
"""
|
|
72
|
+
Handle a document.
|
|
73
|
+
"""
|
|
57
74
|
|
|
58
75
|
|
|
59
76
|
class YoloBox:
|
|
@@ -254,3 +271,28 @@ class OnnxModel(DocLayoutModel):
|
|
|
254
271
|
results.append(YoloResult(boxes_data=preds, names=self._names))
|
|
255
272
|
|
|
256
273
|
return results
|
|
274
|
+
|
|
275
|
+
def handle_document(
|
|
276
|
+
self,
|
|
277
|
+
pages: list[babeldoc.document_il.il_version_1.Page],
|
|
278
|
+
mupdf_doc: pymupdf.Document,
|
|
279
|
+
translate_config,
|
|
280
|
+
save_debug_image,
|
|
281
|
+
) -> Generator[
|
|
282
|
+
tuple[babeldoc.document_il.il_version_1.Page, YoloResult], None, None
|
|
283
|
+
]:
|
|
284
|
+
for page in pages:
|
|
285
|
+
translate_config.raise_if_cancelled()
|
|
286
|
+
pix = mupdf_doc[page.page_number].get_pixmap(dpi=72)
|
|
287
|
+
image = np.fromstring(pix.samples, np.uint8).reshape(
|
|
288
|
+
pix.height,
|
|
289
|
+
pix.width,
|
|
290
|
+
3,
|
|
291
|
+
)[:, :, ::-1]
|
|
292
|
+
predict_result = self.predict(image)[0]
|
|
293
|
+
save_debug_image(
|
|
294
|
+
image,
|
|
295
|
+
predict_result,
|
|
296
|
+
page.page_number + 1,
|
|
297
|
+
)
|
|
298
|
+
yield page, predict_result
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import httpx
|
|
7
|
+
import msgpack
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pymupdf
|
|
10
|
+
from tenacity import retry
|
|
11
|
+
from tenacity import retry_if_exception_type
|
|
12
|
+
from tenacity import stop_after_attempt
|
|
13
|
+
from tenacity import wait_exponential
|
|
14
|
+
|
|
15
|
+
import babeldoc
|
|
16
|
+
from babeldoc.docvision.doclayout import DocLayoutModel
|
|
17
|
+
from babeldoc.docvision.doclayout import YoloBox
|
|
18
|
+
from babeldoc.docvision.doclayout import YoloResult
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def encode_image(image) -> bytes:
|
|
24
|
+
"""Read and encode image to bytes
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
image: Can be either a file path (str) or numpy array
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(image, str):
|
|
30
|
+
if not Path(image).exists():
|
|
31
|
+
raise FileNotFoundError(f"Image file not found: {image}")
|
|
32
|
+
img = cv2.imread(image)
|
|
33
|
+
if img is None:
|
|
34
|
+
raise ValueError(f"Failed to read image: {image}")
|
|
35
|
+
else:
|
|
36
|
+
img = image
|
|
37
|
+
|
|
38
|
+
# logger.debug(f"Image shape: {img.shape}")
|
|
39
|
+
encoded = cv2.imencode(".jpg", img)[1].tobytes()
|
|
40
|
+
# logger.debug(f"Encoded image size: {len(encoded)} bytes")
|
|
41
|
+
return encoded
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@retry(
|
|
45
|
+
stop=stop_after_attempt(3), # 最多重试3次
|
|
46
|
+
wait=wait_exponential(
|
|
47
|
+
multiplier=1, min=1, max=10
|
|
48
|
+
), # 指数退避策略,初始1秒,最大10秒
|
|
49
|
+
retry=retry_if_exception_type((httpx.HTTPError, Exception)), # 针对哪些异常重试
|
|
50
|
+
before_sleep=lambda retry_state: logger.warning(
|
|
51
|
+
f"Request failed, retrying in {retry_state.next_action.sleep} seconds... "
|
|
52
|
+
f"(Attempt {retry_state.attempt_number}/3)"
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
def predict_layout(
|
|
56
|
+
image,
|
|
57
|
+
host: str = "http://localhost:8000",
|
|
58
|
+
imgsz: int = 1024,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Predict document layout using the MOSEC service
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
image: Can be either a file path (str) or numpy array
|
|
65
|
+
host: Service host URL
|
|
66
|
+
imgsz: Image size for model input
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of predictions containing bounding boxes and classes
|
|
70
|
+
"""
|
|
71
|
+
# Prepare request data
|
|
72
|
+
if not isinstance(image, list):
|
|
73
|
+
image = [image]
|
|
74
|
+
image_data = [encode_image(image) for image in image]
|
|
75
|
+
data = {
|
|
76
|
+
"image": image_data,
|
|
77
|
+
"imgsz": imgsz,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Pack data using msgpack
|
|
81
|
+
packed_data = msgpack.packb(data, use_bin_type=True)
|
|
82
|
+
logger.debug(f"Packed data size: {len(packed_data)} bytes")
|
|
83
|
+
|
|
84
|
+
# Send request
|
|
85
|
+
logger.debug(f"Sending request to {host}/inference")
|
|
86
|
+
response = httpx.post(
|
|
87
|
+
f"{host}/inference",
|
|
88
|
+
data=packed_data,
|
|
89
|
+
headers={
|
|
90
|
+
"Content-Type": "application/msgpack",
|
|
91
|
+
"Accept": "application/msgpack",
|
|
92
|
+
},
|
|
93
|
+
timeout=300,
|
|
94
|
+
follow_redirects=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Response status: {response.status_code}")
|
|
98
|
+
logger.debug(f"Response headers: {response.headers}")
|
|
99
|
+
|
|
100
|
+
if response.status_code == 200:
|
|
101
|
+
try:
|
|
102
|
+
result = msgpack.unpackb(response.content, raw=False)
|
|
103
|
+
return result
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.exception(f"Failed to unpack response: {e!s}")
|
|
106
|
+
raise
|
|
107
|
+
else:
|
|
108
|
+
logger.error(f"Request failed with status {response.status_code}")
|
|
109
|
+
logger.error(f"Response content: {response.content}")
|
|
110
|
+
raise Exception(
|
|
111
|
+
f"Request failed with status {response.status_code}: {response.text}",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ResultContainer:
|
|
116
|
+
def __init__(self):
|
|
117
|
+
self.result = YoloResult(boxes_data=np.array([]), names=[])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RpcDocLayoutModel(DocLayoutModel):
|
|
121
|
+
"""DocLayoutModel implementation that uses RPC service."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, host: str = "http://localhost:8000"):
|
|
124
|
+
"""Initialize RPC model with host address."""
|
|
125
|
+
self.host = host
|
|
126
|
+
self._stride = 32 # Default stride value
|
|
127
|
+
self._names = ["text", "title", "list", "table", "figure"]
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def stride(self) -> int:
|
|
131
|
+
"""Stride of the model input."""
|
|
132
|
+
return self._stride
|
|
133
|
+
|
|
134
|
+
def resize_and_pad_image(self, image, new_shape):
|
|
135
|
+
"""
|
|
136
|
+
Resize and pad the image to the specified size,
|
|
137
|
+
ensuring dimensions are multiples of stride.
|
|
138
|
+
|
|
139
|
+
Parameters:
|
|
140
|
+
- image: Input image
|
|
141
|
+
- new_shape: Target size (integer or (height, width) tuple)
|
|
142
|
+
- stride: Padding alignment stride, default 32
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
- Processed image
|
|
146
|
+
"""
|
|
147
|
+
if isinstance(new_shape, int):
|
|
148
|
+
new_shape = (new_shape, new_shape)
|
|
149
|
+
|
|
150
|
+
h, w = image.shape[:2]
|
|
151
|
+
new_h, new_w = new_shape
|
|
152
|
+
|
|
153
|
+
# Calculate scaling ratio
|
|
154
|
+
r = min(new_h / h, new_w / w)
|
|
155
|
+
resized_h, resized_w = int(round(h * r)), int(round(w * r))
|
|
156
|
+
|
|
157
|
+
# Resize image
|
|
158
|
+
image = cv2.resize(
|
|
159
|
+
image, (resized_w, resized_h), interpolation=cv2.INTER_LINEAR
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Calculate padding size
|
|
163
|
+
pad_h = new_h - resized_h
|
|
164
|
+
pad_w = new_w - resized_w
|
|
165
|
+
top, bottom = pad_h // 2, pad_h - pad_h // 2
|
|
166
|
+
left, right = pad_w // 2, pad_w - pad_w // 2
|
|
167
|
+
|
|
168
|
+
# Add padding
|
|
169
|
+
image = cv2.copyMakeBorder(
|
|
170
|
+
image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return image
|
|
174
|
+
|
|
175
|
+
def scale_boxes(self, img1_shape, boxes, img0_shape):
|
|
176
|
+
"""
|
|
177
|
+
Rescales bounding boxes (in the format of xyxy by default) from the shape of the image they were originally
|
|
178
|
+
specified in (img1_shape) to the shape of a different image (img0_shape).
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
img1_shape (tuple): The shape of the image that the bounding boxes are for,
|
|
182
|
+
in the format of (height, width).
|
|
183
|
+
boxes (torch.Tensor): the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2)
|
|
184
|
+
img0_shape (tuple): the shape of the target image, in the format of (height, width).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
boxes (torch.Tensor): The scaled bounding boxes, in the format of (x1, y1, x2, y2)
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
# Calculate scaling ratio
|
|
191
|
+
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
|
|
192
|
+
|
|
193
|
+
# Calculate padding size
|
|
194
|
+
pad_x = round((img1_shape[1] - img0_shape[1] * gain) / 2 - 0.1)
|
|
195
|
+
pad_y = round((img1_shape[0] - img0_shape[0] * gain) / 2 - 0.1)
|
|
196
|
+
|
|
197
|
+
# Remove padding and scale boxes
|
|
198
|
+
boxes = (boxes - [pad_x, pad_y, pad_x, pad_y]) / gain
|
|
199
|
+
return boxes
|
|
200
|
+
|
|
201
|
+
def predict_image(
|
|
202
|
+
self,
|
|
203
|
+
image,
|
|
204
|
+
host: str = None,
|
|
205
|
+
result_container: ResultContainer | None = None,
|
|
206
|
+
imgsz: int = 1024,
|
|
207
|
+
) -> ResultContainer:
|
|
208
|
+
"""Predict the layout of document pages using RPC service."""
|
|
209
|
+
if result_container is None:
|
|
210
|
+
result_container = ResultContainer()
|
|
211
|
+
target_imgsz = (800, 800)
|
|
212
|
+
orig_h, orig_w = image.shape[:2]
|
|
213
|
+
if image.shape[0] != target_imgsz[0] or image.shape[1] != target_imgsz[1]:
|
|
214
|
+
image = self.resize_and_pad_image(image, new_shape=target_imgsz)
|
|
215
|
+
preds = predict_layout([image], host=self.host, imgsz=800)
|
|
216
|
+
|
|
217
|
+
if len(preds) > 0:
|
|
218
|
+
for pred in preds:
|
|
219
|
+
boxes = [
|
|
220
|
+
YoloBox(
|
|
221
|
+
None,
|
|
222
|
+
self.scale_boxes(
|
|
223
|
+
(800, 800), np.array(x["xyxy"]), (orig_h, orig_w)
|
|
224
|
+
),
|
|
225
|
+
np.array(x["conf"]),
|
|
226
|
+
x["cls"],
|
|
227
|
+
)
|
|
228
|
+
for x in pred["boxes"]
|
|
229
|
+
]
|
|
230
|
+
result_container.result = YoloResult(
|
|
231
|
+
boxes=boxes,
|
|
232
|
+
names={int(k): v for k, v in pred["names"].items()},
|
|
233
|
+
)
|
|
234
|
+
return result_container.result
|
|
235
|
+
|
|
236
|
+
def predict(self, image, imgsz=1024, **kwargs) -> list[YoloResult]:
|
|
237
|
+
"""Predict the layout of document pages using RPC service."""
|
|
238
|
+
# Handle single image input
|
|
239
|
+
if isinstance(image, np.ndarray) and len(image.shape) == 3:
|
|
240
|
+
image = [image]
|
|
241
|
+
|
|
242
|
+
result_containers = [ResultContainer() for _ in image]
|
|
243
|
+
predict_thread = ThreadPoolExecutor(max_workers=len(image))
|
|
244
|
+
for img, result_container in zip(image, result_containers, strict=True):
|
|
245
|
+
predict_thread.submit(
|
|
246
|
+
self.predict_image, img, self.host, result_container, 800
|
|
247
|
+
)
|
|
248
|
+
predict_thread.shutdown(wait=True)
|
|
249
|
+
result = [result_container.result for result_container in result_containers]
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def predict_page(
|
|
253
|
+
self, page, mupdf_doc: pymupdf.Document, translate_config, save_debug_image
|
|
254
|
+
):
|
|
255
|
+
translate_config.raise_if_cancelled()
|
|
256
|
+
pix = mupdf_doc[page.page_number].get_pixmap(dpi=72)
|
|
257
|
+
image = np.fromstring(pix.samples, np.uint8).reshape(
|
|
258
|
+
pix.height,
|
|
259
|
+
pix.width,
|
|
260
|
+
3,
|
|
261
|
+
)[:, :, ::-1]
|
|
262
|
+
predict_result = self.predict_image(image, self.host, None, 800)
|
|
263
|
+
save_debug_image(image, predict_result, page.page_number + 1)
|
|
264
|
+
return page, predict_result
|
|
265
|
+
|
|
266
|
+
def handle_document(
|
|
267
|
+
self,
|
|
268
|
+
pages: list[babeldoc.document_il.il_version_1.Page],
|
|
269
|
+
mupdf_doc: pymupdf.Document,
|
|
270
|
+
translate_config,
|
|
271
|
+
save_debug_image,
|
|
272
|
+
):
|
|
273
|
+
with ThreadPoolExecutor(max_workers=16) as executor:
|
|
274
|
+
yield from executor.map(
|
|
275
|
+
self.predict_page,
|
|
276
|
+
pages,
|
|
277
|
+
(mupdf_doc for _ in range(len(pages))),
|
|
278
|
+
(translate_config for _ in range(len(pages))),
|
|
279
|
+
(save_debug_image for _ in range(len(pages))),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def from_host(host: str) -> "RpcDocLayoutModel":
|
|
284
|
+
"""Create RpcDocLayoutModel from host address."""
|
|
285
|
+
return RpcDocLayoutModel(host=host)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
290
|
+
# Test the service
|
|
291
|
+
try:
|
|
292
|
+
# Use a default test image if example/1.png doesn't exist
|
|
293
|
+
image_path = "example/1.png"
|
|
294
|
+
if not Path(image_path).exists():
|
|
295
|
+
print(f"Warning: {image_path} not found.")
|
|
296
|
+
print("Please provide the path to a test image:")
|
|
297
|
+
image_path = input("> ")
|
|
298
|
+
|
|
299
|
+
logger.info(f"Processing image: {image_path}")
|
|
300
|
+
result = predict_layout(image_path)
|
|
301
|
+
print("Prediction results:")
|
|
302
|
+
print(result)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
print(f"Error: {e!s}")
|
|
@@ -17,6 +17,7 @@ import babeldoc.high_level
|
|
|
17
17
|
from babeldoc.document_il.translator.translator import BingTranslator
|
|
18
18
|
from babeldoc.document_il.translator.translator import GoogleTranslator
|
|
19
19
|
from babeldoc.document_il.translator.translator import OpenAITranslator
|
|
20
|
+
from babeldoc.document_il.translator.translator import TranslateTranslator
|
|
20
21
|
from babeldoc.document_il.translator.translator import set_translate_rate_limiter
|
|
21
22
|
from babeldoc.docvision.doclayout import DocLayoutModel
|
|
22
23
|
from babeldoc.docvision.rpc_doclayout import RpcDocLayoutModel
|
|
@@ -24,7 +25,7 @@ from babeldoc.translation_config import TranslationConfig
|
|
|
24
25
|
from babeldoc.translation_config import WatermarkOutputMode
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
|
-
__version__ = "0.1.
|
|
28
|
+
__version__ = "0.1.27"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def create_parser():
|
|
@@ -200,6 +201,11 @@ def create_parser():
|
|
|
200
201
|
action="store_true",
|
|
201
202
|
help="Use Google translator.",
|
|
202
203
|
)
|
|
204
|
+
service_group.add_argument(
|
|
205
|
+
"--translate",
|
|
206
|
+
action="store_true",
|
|
207
|
+
help="Use translate translator.",
|
|
208
|
+
)
|
|
203
209
|
service_group.add_argument(
|
|
204
210
|
"--bing",
|
|
205
211
|
action="store_true",
|
|
@@ -223,6 +229,14 @@ def create_parser():
|
|
|
223
229
|
"-k",
|
|
224
230
|
help="The API key for the OpenAI API.",
|
|
225
231
|
)
|
|
232
|
+
service_group = parser.add_argument_group(
|
|
233
|
+
"Translation - Translate Options",
|
|
234
|
+
description="Translate specific options",
|
|
235
|
+
)
|
|
236
|
+
service_group.add_argument(
|
|
237
|
+
"--translate-url",
|
|
238
|
+
help="The base URL for the Translation API.",
|
|
239
|
+
)
|
|
226
240
|
|
|
227
241
|
return parser
|
|
228
242
|
|
|
@@ -254,8 +268,8 @@ async def main():
|
|
|
254
268
|
return
|
|
255
269
|
|
|
256
270
|
# 验证翻译服务选择
|
|
257
|
-
if not (args.openai or args.google or args.bing):
|
|
258
|
-
parser.error("必须选择一个翻译服务:--openai、--google 或 --
|
|
271
|
+
if not (args.openai or args.google or args.bing or args.translate):
|
|
272
|
+
parser.error("必须选择一个翻译服务:--openai、--google、--bing 或 --translate")
|
|
259
273
|
|
|
260
274
|
# 验证 OpenAI 参数
|
|
261
275
|
if args.openai and not args.openai_api_key:
|
|
@@ -277,6 +291,13 @@ async def main():
|
|
|
277
291
|
lang_out=args.lang_out,
|
|
278
292
|
ignore_cache=args.ignore_cache,
|
|
279
293
|
)
|
|
294
|
+
elif args.translate:
|
|
295
|
+
translator = TranslateTranslator(
|
|
296
|
+
lang_in=args.lang_in,
|
|
297
|
+
lang_out=args.lang_out,
|
|
298
|
+
ignore_cache=args.ignore_cache,
|
|
299
|
+
url=args.translate_url,
|
|
300
|
+
)
|
|
280
301
|
else:
|
|
281
302
|
translator = GoogleTranslator(
|
|
282
303
|
lang_in=args.lang_in,
|
|
@@ -321,6 +342,16 @@ async def main():
|
|
|
321
342
|
else:
|
|
322
343
|
args.output = None
|
|
323
344
|
|
|
345
|
+
watermark_output_mode = WatermarkOutputMode.Watermarked
|
|
346
|
+
if args.no_watermark:
|
|
347
|
+
watermark_output_mode = WatermarkOutputMode.NoWatermark
|
|
348
|
+
elif args.watermark_output_mode == "both":
|
|
349
|
+
watermark_output_mode = WatermarkOutputMode.Both
|
|
350
|
+
elif args.watermark_output_mode == "watermarked":
|
|
351
|
+
watermark_output_mode = WatermarkOutputMode.Watermarked
|
|
352
|
+
elif args.watermark_output_mode == "no_watermark":
|
|
353
|
+
watermark_output_mode = WatermarkOutputMode.NoWatermark
|
|
354
|
+
|
|
324
355
|
for file in pending_files:
|
|
325
356
|
# 清理文件路径,去除两端的引号
|
|
326
357
|
file = file.strip("\"'")
|
|
@@ -349,11 +380,7 @@ async def main():
|
|
|
349
380
|
use_alternating_pages_dual=args.use_alternating_pages_dual,
|
|
350
381
|
report_interval=args.report_interval,
|
|
351
382
|
min_text_length=args.min_text_length,
|
|
352
|
-
watermark_output_mode=
|
|
353
|
-
WatermarkOutputMode.NoWatermark
|
|
354
|
-
if args.no_watermark
|
|
355
|
-
else getattr(WatermarkOutputMode, args.watermark_output_mode.title())
|
|
356
|
-
),
|
|
383
|
+
watermark_output_mode=watermark_output_mode,
|
|
357
384
|
)
|
|
358
385
|
|
|
359
386
|
# Create progress handler
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "BabelDOC"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.27"
|
|
4
4
|
description = "Yet Another Document Translator"
|
|
5
5
|
license = "AGPL-3.0"
|
|
6
6
|
readme = "README.md"
|
|
@@ -133,7 +133,7 @@ pythonpath = [".", "src"]
|
|
|
133
133
|
testpaths = ["tests"]
|
|
134
134
|
|
|
135
135
|
[bumpver]
|
|
136
|
-
current_version = "0.1.
|
|
136
|
+
current_version = "0.1.27"
|
|
137
137
|
version_pattern = "MAJOR.MINOR.PATCH[.PYTAGNUM]"
|
|
138
138
|
|
|
139
139
|
[bumpver.file_patterns]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.26"
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import cv2
|
|
5
|
-
import httpx
|
|
6
|
-
import msgpack
|
|
7
|
-
import numpy as np
|
|
8
|
-
from tenacity import retry
|
|
9
|
-
from tenacity import retry_if_exception_type
|
|
10
|
-
from tenacity import stop_after_attempt
|
|
11
|
-
from tenacity import wait_exponential
|
|
12
|
-
|
|
13
|
-
from babeldoc.docvision.doclayout import DocLayoutModel
|
|
14
|
-
from babeldoc.docvision.doclayout import YoloBox
|
|
15
|
-
from babeldoc.docvision.doclayout import YoloResult
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def encode_image(image) -> bytes:
|
|
21
|
-
"""Read and encode image to bytes
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
image: Can be either a file path (str) or numpy array
|
|
25
|
-
"""
|
|
26
|
-
if isinstance(image, str):
|
|
27
|
-
if not Path(image).exists():
|
|
28
|
-
raise FileNotFoundError(f"Image file not found: {image}")
|
|
29
|
-
img = cv2.imread(image)
|
|
30
|
-
if img is None:
|
|
31
|
-
raise ValueError(f"Failed to read image: {image}")
|
|
32
|
-
else:
|
|
33
|
-
img = image
|
|
34
|
-
|
|
35
|
-
# logger.debug(f"Image shape: {img.shape}")
|
|
36
|
-
encoded = cv2.imencode(".jpg", img)[1].tobytes()
|
|
37
|
-
# logger.debug(f"Encoded image size: {len(encoded)} bytes")
|
|
38
|
-
return encoded
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@retry(
|
|
42
|
-
stop=stop_after_attempt(3), # 最多重试3次
|
|
43
|
-
wait=wait_exponential(
|
|
44
|
-
multiplier=1, min=1, max=10
|
|
45
|
-
), # 指数退避策略,初始1秒,最大10秒
|
|
46
|
-
retry=retry_if_exception_type((httpx.HTTPError, Exception)), # 针对哪些异常重试
|
|
47
|
-
before_sleep=lambda retry_state: logger.warning(
|
|
48
|
-
f"Request failed, retrying in {retry_state.next_action.sleep} seconds... "
|
|
49
|
-
f"(Attempt {retry_state.attempt_number}/3)"
|
|
50
|
-
),
|
|
51
|
-
)
|
|
52
|
-
def predict_layout(
|
|
53
|
-
image,
|
|
54
|
-
host: str = "http://localhost:8000",
|
|
55
|
-
imgsz: int = 1024,
|
|
56
|
-
):
|
|
57
|
-
"""
|
|
58
|
-
Predict document layout using the MOSEC service
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
image: Can be either a file path (str) or numpy array
|
|
62
|
-
host: Service host URL
|
|
63
|
-
imgsz: Image size for model input
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
List of predictions containing bounding boxes and classes
|
|
67
|
-
"""
|
|
68
|
-
# Prepare request data
|
|
69
|
-
if not isinstance(image, list):
|
|
70
|
-
image = [image]
|
|
71
|
-
image_data = [encode_image(image) for image in image]
|
|
72
|
-
data = {
|
|
73
|
-
"image": image_data,
|
|
74
|
-
"imgsz": imgsz,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
# Pack data using msgpack
|
|
78
|
-
packed_data = msgpack.packb(data, use_bin_type=True)
|
|
79
|
-
logger.debug(f"Packed data size: {len(packed_data)} bytes")
|
|
80
|
-
|
|
81
|
-
# Send request
|
|
82
|
-
logger.debug(f"Sending request to {host}/inference")
|
|
83
|
-
response = httpx.post(
|
|
84
|
-
f"{host}/inference",
|
|
85
|
-
data=packed_data,
|
|
86
|
-
headers={
|
|
87
|
-
"Content-Type": "application/msgpack",
|
|
88
|
-
"Accept": "application/msgpack",
|
|
89
|
-
},
|
|
90
|
-
timeout=300,
|
|
91
|
-
follow_redirects=True,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
logger.debug(f"Response status: {response.status_code}")
|
|
95
|
-
logger.debug(f"Response headers: {response.headers}")
|
|
96
|
-
|
|
97
|
-
if response.status_code == 200:
|
|
98
|
-
try:
|
|
99
|
-
result = msgpack.unpackb(response.content, raw=False)
|
|
100
|
-
return result
|
|
101
|
-
except Exception as e:
|
|
102
|
-
logger.exception(f"Failed to unpack response: {e!s}")
|
|
103
|
-
raise
|
|
104
|
-
else:
|
|
105
|
-
logger.error(f"Request failed with status {response.status_code}")
|
|
106
|
-
logger.error(f"Response content: {response.content}")
|
|
107
|
-
raise Exception(
|
|
108
|
-
f"Request failed with status {response.status_code}: {response.text}",
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class RpcDocLayoutModel(DocLayoutModel):
|
|
113
|
-
"""DocLayoutModel implementation that uses RPC service."""
|
|
114
|
-
|
|
115
|
-
def __init__(self, host: str = "http://localhost:8000"):
|
|
116
|
-
"""Initialize RPC model with host address."""
|
|
117
|
-
self.host = host
|
|
118
|
-
self._stride = 32 # Default stride value
|
|
119
|
-
self._names = ["text", "title", "list", "table", "figure"]
|
|
120
|
-
|
|
121
|
-
@property
|
|
122
|
-
def stride(self) -> int:
|
|
123
|
-
"""Stride of the model input."""
|
|
124
|
-
return self._stride
|
|
125
|
-
|
|
126
|
-
def predict(self, image, imgsz=1024, **kwargs) -> list[YoloResult]:
|
|
127
|
-
"""Predict the layout of document pages using RPC service."""
|
|
128
|
-
# Handle single image input
|
|
129
|
-
if isinstance(image, np.ndarray) and len(image.shape) == 3:
|
|
130
|
-
image = [image]
|
|
131
|
-
|
|
132
|
-
results = []
|
|
133
|
-
for img in image:
|
|
134
|
-
preds = predict_layout([img], host=self.host, imgsz=imgsz)
|
|
135
|
-
if len(preds) > 0:
|
|
136
|
-
for pred in preds:
|
|
137
|
-
boxes = [
|
|
138
|
-
YoloBox(
|
|
139
|
-
None, np.array(x["xyxy"]), np.array(x["conf"]), x["cls"]
|
|
140
|
-
)
|
|
141
|
-
for x in pred["boxes"]
|
|
142
|
-
]
|
|
143
|
-
results.append(
|
|
144
|
-
YoloResult(
|
|
145
|
-
boxes=boxes,
|
|
146
|
-
names={int(k): v for k, v in pred["names"].items()},
|
|
147
|
-
),
|
|
148
|
-
)
|
|
149
|
-
else:
|
|
150
|
-
results.append(YoloResult(boxes_data=np.array([]), names=[]))
|
|
151
|
-
|
|
152
|
-
return results
|
|
153
|
-
|
|
154
|
-
@staticmethod
|
|
155
|
-
def from_host(host: str) -> "RpcDocLayoutModel":
|
|
156
|
-
"""Create RpcDocLayoutModel from host address."""
|
|
157
|
-
return RpcDocLayoutModel(host=host)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if __name__ == "__main__":
|
|
161
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
162
|
-
# Test the service
|
|
163
|
-
try:
|
|
164
|
-
# Use a default test image if example/1.png doesn't exist
|
|
165
|
-
image_path = "example/1.png"
|
|
166
|
-
if not Path(image_path).exists():
|
|
167
|
-
print(f"Warning: {image_path} not found.")
|
|
168
|
-
print("Please provide the path to a test image:")
|
|
169
|
-
image_path = input("> ")
|
|
170
|
-
|
|
171
|
-
logger.info(f"Processing image: {image_path}")
|
|
172
|
-
result = predict_layout(image_path)
|
|
173
|
-
print("Prediction results:")
|
|
174
|
-
print(result)
|
|
175
|
-
except Exception as e:
|
|
176
|
-
print(f"Error: {e!s}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo-with-transparent-background.png
RENAMED
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-big-logo-with-transparent-background.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-small-logo-with-transparent-background.png
RENAMED
|
File without changes
|
{babeldoc-0.1.26 → babeldoc-0.1.27}/docs/images/babeldoc-small-logo-with-transparent-background.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|