BabelDOC 0.1.26__tar.gz → 0.1.28__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.
Files changed (91) hide show
  1. {babeldoc-0.1.26 → babeldoc-0.1.28}/PKG-INFO +3 -2
  2. {babeldoc-0.1.26 → babeldoc-0.1.28}/README.md +2 -1
  3. babeldoc-0.1.28/babeldoc/__init__.py +1 -0
  4. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/const.py +1 -1
  5. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/il_translator.py +2 -0
  6. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/layout_parser.py +33 -61
  7. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/paragraph_finder.py +2 -0
  8. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/styles_and_formulas.py +1 -3
  9. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/translator/translator.py +50 -0
  10. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/docvision/doclayout.py +54 -12
  11. babeldoc-0.1.28/babeldoc/docvision/rpc_doclayout.py +304 -0
  12. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/main.py +35 -8
  13. {babeldoc-0.1.26 → babeldoc-0.1.28}/pyproject.toml +2 -2
  14. {babeldoc-0.1.26 → babeldoc-0.1.28}/uv.lock +1 -1
  15. babeldoc-0.1.26/babeldoc/__init__.py +0 -1
  16. babeldoc-0.1.26/babeldoc/docvision/rpc_doclayout.py +0 -176
  17. {babeldoc-0.1.26 → babeldoc-0.1.28}/.cursorignore +0 -0
  18. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  19. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  20. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/dependabot.yml +0 -0
  21. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/labels.yml +0 -0
  22. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/release-drafter.yml +0 -0
  23. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/workflows/codeql.yml +0 -0
  24. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/workflows/docs.yml +0 -0
  25. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/workflows/labeler.yml +0 -0
  26. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/workflows/publish-to-pypi.yml +0 -0
  27. {babeldoc-0.1.26 → babeldoc-0.1.28}/.github/workflows/test.yml +0 -0
  28. {babeldoc-0.1.26 → babeldoc-0.1.28}/.gitignore +0 -0
  29. {babeldoc-0.1.26 → babeldoc-0.1.28}/.pre-commit-config.yaml +0 -0
  30. {babeldoc-0.1.26 → babeldoc-0.1.28}/LICENSE +0 -0
  31. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/assets/assets.py +0 -0
  32. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/assets/embedding_assets_metadata.py +0 -0
  33. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/asynchronize/__init__.py +0 -0
  34. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/converter.py +0 -0
  35. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/__init__.py +0 -0
  36. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
  37. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/backend/__init__.py +0 -0
  38. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/backend/pdf_creater.py +0 -0
  39. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/frontend/__init__.py +0 -0
  40. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/frontend/il_creater.py +0 -0
  41. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/il_version_1.py +0 -0
  42. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/il_version_1.rnc +0 -0
  43. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/il_version_1.rng +0 -0
  44. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/il_version_1.xsd +0 -0
  45. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/__init__.py +0 -0
  46. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/add_debug_information.py +0 -0
  47. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
  48. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/remove_descent.py +0 -0
  49. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/midend/typesetting.py +0 -0
  50. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/translator/__init__.py +0 -0
  51. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/translator/cache.py +0 -0
  52. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/utils/fontmap.py +0 -0
  53. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/utils/layout_helper.py +0 -0
  54. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/utils/style_helper.py +0 -0
  55. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/document_il/xml_converter.py +0 -0
  56. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/docvision/README.md +0 -0
  57. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/docvision/__init__.py +0 -0
  58. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/high_level.py +0 -0
  59. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/pdfinterp.py +0 -0
  60. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/progress_monitor.py +0 -0
  61. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/tools/generate_font_metadata.py +0 -0
  62. {babeldoc-0.1.26 → babeldoc-0.1.28}/babeldoc/translation_config.py +0 -0
  63. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/CODE_OF_CONDUCT.md +0 -0
  64. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/CONTRIBUTING.md +0 -0
  65. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/CONTRIBUTOR_REWARD.md +0 -0
  66. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
  67. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
  68. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
  69. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
  70. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
  71. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/README.md +0 -0
  72. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
  73. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
  74. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/README.md +0 -0
  75. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/deploy.sh +0 -0
  76. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-banner.png +0 -0
  77. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
  78. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
  79. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
  80. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
  81. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-big-logo.png +0 -0
  82. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-contributor_reward_example.png +0 -0
  83. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-preview.gif +0 -0
  84. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
  85. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
  86. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/images/babeldoc-small-logo.png +0 -0
  87. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/index.md +0 -0
  88. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/intro-to-pdf-object.md +0 -0
  89. {babeldoc-0.1.26 → babeldoc-0.1.28}/docs/requirements.txt +0 -0
  90. {babeldoc-0.1.26 → babeldoc-0.1.28}/mkdocs.yml +0 -0
  91. {babeldoc-0.1.26 → babeldoc-0.1.28}/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.26
3
+ Version: 0.1.28
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.28"
@@ -2,7 +2,7 @@ import shutil
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
 
5
- __version__ = "0.1.26"
5
+ __version__ = "0.1.28"
6
6
 
7
7
  CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"
8
8
 
@@ -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
- pages_to_translate = [
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 pages in batches
124
- batch_size = 1
125
- for i in range(0, total, batch_size):
126
- self.translation_config.raise_if_cancelled()
127
- batch_pages = pages_to_translate[i : i + batch_size]
128
-
129
- # Prepare batch images
130
- batch_images = []
131
- for page in batch_pages:
132
- pix = mupdf_doc[page.page_number].get_pixmap(dpi=72)
133
- image = np.fromstring(pix.samples, np.uint8).reshape(
134
- pix.height,
135
- pix.width,
136
- 3,
137
- )[:, :, ::-1]
138
- batch_images.append(image)
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
- for layout in layouts.boxes:
152
- # Convert coordinate system from picture to il
153
- # system to the il coordinate system
154
- x0, y0, x1, y1 = layout.xyxy
155
- pix = mupdf_doc[page.page_number].get_pixmap()
156
- h, w = pix.height, pix.width
157
- x0, y0, x1, y1 = (
158
- np.clip(int(x0 - 1), 0, w - 1),
159
- np.clip(int(h - y1 - 1), 0, h - 1),
160
- np.clip(int(x1 + 1), 0, w - 1),
161
- np.clip(int(h - y0 + 1), 0, h - 1),
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
- page.page_layout = page_layouts
177
- self._save_debug_box_to_page(page)
178
- progress.advance(1)
148
+ page.page_layout = page_layouts
149
+ self._save_debug_box_to_page(page)
150
+ progress.advance(1)
179
151
 
180
- return docs
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
@@ -479,9 +479,7 @@ class StylesAndFormulas:
479
479
  return bool(re.match(r"^[0-9, ]+$", text))
480
480
 
481
481
  def is_formulas_font(self, font_name: str) -> bool:
482
- pattern2 = (
483
- r"^(Cambria|Cambria-BoldItalic|Cambria-Bold|Cambria-Italic|EUAlbertina.+)$"
484
- )
482
+ pattern2 = r"^(Cambria|Cambria-BoldItalic|Cambria-Bold|Cambria-Italic|EUAlbertina.+|NimbusRomNo9L.+)$"
485
483
  if self.translation_config.formular_font_pattern:
486
484
  pattern = self.translation_config.formular_font_pattern
487
485
  else:
@@ -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
- class YoloResult:
47
- """Helper class to store detection results from ONNX model."""
48
-
49
- def __init__(self, names, boxes=None, boxes_data=None):
50
- if boxes is not None:
51
- self.boxes = boxes
52
- else:
53
- assert boxes_data is not None
54
- self.boxes = [YoloBox(data=d) for d in boxes_data]
55
- self.boxes.sort(key=lambda x: x.conf, reverse=True)
56
- self.names = names
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.26"
28
+ __version__ = "0.1.28"
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 或 --bing")
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.26"
3
+ version = "0.1.28"
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.26"
136
+ current_version = "0.1.28"
137
137
  version_pattern = "MAJOR.MINOR.PATCH[.PYTAGNUM]"
138
138
 
139
139
  [bumpver.file_patterns]
@@ -48,7 +48,7 @@ wheels = [
48
48
 
49
49
  [[package]]
50
50
  name = "babeldoc"
51
- version = "0.1.26"
51
+ version = "0.1.28"
52
52
  source = { editable = "." }
53
53
  dependencies = [
54
54
  { name = "bitstring" },
@@ -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