BabelDOC 0.2.14__tar.gz → 0.2.15__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 (95) hide show
  1. {babeldoc-0.2.14 → babeldoc-0.2.15}/PKG-INFO +3 -1
  2. {babeldoc-0.2.14 → babeldoc-0.2.15}/README.md +1 -0
  3. babeldoc-0.2.15/babeldoc/__init__.py +1 -0
  4. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/assets/assets.py +35 -1
  5. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/assets/embedding_assets_metadata.py +10 -0
  6. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/const.py +1 -1
  7. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/paragraph_finder.py +2 -0
  8. babeldoc-0.2.15/babeldoc/document_il/midend/table_parser.py +164 -0
  9. babeldoc-0.2.15/babeldoc/docvision/table_detection/rapidocr.py +316 -0
  10. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/high_level.py +11 -0
  11. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/main.py +14 -1
  12. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/translation_config.py +3 -0
  13. {babeldoc-0.2.14 → babeldoc-0.2.15}/pyproject.toml +3 -2
  14. {babeldoc-0.2.14 → babeldoc-0.2.15}/uv.lock +94 -1
  15. babeldoc-0.2.14/babeldoc/__init__.py +0 -1
  16. {babeldoc-0.2.14 → babeldoc-0.2.15}/.cursorignore +0 -0
  17. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  18. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  19. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/dependabot.yml +0 -0
  20. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/labels.yml +0 -0
  21. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/release-drafter.yml +0 -0
  22. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/workflows/codeql.yml +0 -0
  23. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/workflows/docs.yml +0 -0
  24. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/workflows/labeler.yml +0 -0
  25. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/workflows/publish-to-pypi.yml +0 -0
  26. {babeldoc-0.2.14 → babeldoc-0.2.15}/.github/workflows/test.yml +0 -0
  27. {babeldoc-0.2.14 → babeldoc-0.2.15}/.gitignore +0 -0
  28. {babeldoc-0.2.14 → babeldoc-0.2.15}/.pre-commit-config.yaml +0 -0
  29. {babeldoc-0.2.14 → babeldoc-0.2.15}/LICENSE +0 -0
  30. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/asynchronize/__init__.py +0 -0
  31. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/converter.py +0 -0
  32. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/__init__.py +0 -0
  33. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
  34. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/backend/__init__.py +0 -0
  35. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/backend/pdf_creater.py +0 -0
  36. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/frontend/__init__.py +0 -0
  37. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/frontend/il_creater.py +0 -0
  38. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/il_version_1.py +0 -0
  39. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/il_version_1.rnc +0 -0
  40. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/il_version_1.rng +0 -0
  41. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/il_version_1.xsd +0 -0
  42. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/__init__.py +0 -0
  43. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/add_debug_information.py +0 -0
  44. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
  45. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/il_translator.py +0 -0
  46. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/il_translator_llm_only.py +0 -0
  47. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/layout_parser.py +0 -0
  48. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/remove_descent.py +0 -0
  49. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/styles_and_formulas.py +0 -0
  50. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/midend/typesetting.py +0 -0
  51. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/translator/__init__.py +0 -0
  52. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/translator/cache.py +0 -0
  53. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/translator/translator.py +0 -0
  54. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/utils/fontmap.py +0 -0
  55. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/utils/layout_helper.py +0 -0
  56. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/utils/style_helper.py +0 -0
  57. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/document_il/xml_converter.py +0 -0
  58. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/docvision/README.md +0 -0
  59. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/docvision/__init__.py +0 -0
  60. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/docvision/doclayout.py +0 -0
  61. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/docvision/rpc_doclayout.py +0 -0
  62. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/pdfinterp.py +0 -0
  63. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/progress_monitor.py +0 -0
  64. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/result_merger.py +0 -0
  65. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/split_manager.py +0 -0
  66. {babeldoc-0.2.14 → babeldoc-0.2.15}/babeldoc/tools/generate_font_metadata.py +0 -0
  67. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/CODE_OF_CONDUCT.md +0 -0
  68. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/CONTRIBUTING.md +0 -0
  69. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/CONTRIBUTOR_REWARD.md +0 -0
  70. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
  71. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
  72. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
  73. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
  74. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
  75. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/README.md +0 -0
  76. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
  77. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
  78. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/README.md +0 -0
  79. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/deploy.sh +0 -0
  80. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-banner.png +0 -0
  81. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
  82. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
  83. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
  84. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
  85. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-big-logo.png +0 -0
  86. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-contributor_reward_example.png +0 -0
  87. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-preview.gif +0 -0
  88. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
  89. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
  90. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/images/babeldoc-small-logo.png +0 -0
  91. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/index.md +0 -0
  92. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/intro-to-pdf-object.md +0 -0
  93. {babeldoc-0.2.14 → babeldoc-0.2.15}/docs/requirements.txt +0 -0
  94. {babeldoc-0.2.14 → babeldoc-0.2.15}/mkdocs.yml +0 -0
  95. {babeldoc-0.2.14 → babeldoc-0.2.15}/tests/test_translation_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: BabelDOC
3
- Version: 0.2.14
3
+ Version: 0.2.15
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
@@ -30,6 +30,7 @@ Requires-Dist: psutil>=7.0.0
30
30
  Requires-Dist: pydantic>=2.10.6
31
31
  Requires-Dist: pymupdf>=1.25.1
32
32
  Requires-Dist: python-levenshtein>=0.27.1
33
+ Requires-Dist: rapidocr-onnxruntime>=1.4.4
33
34
  Requires-Dist: rich>=13.9.4
34
35
  Requires-Dist: scikit-image>=0.25.2
35
36
  Requires-Dist: tenacity>=9.0.0
@@ -181,6 +182,7 @@ uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model
181
182
  - `--watermark-output-mode`: Control watermark output mode: 'watermarked' (default) adds watermark to translated PDF, 'no_watermark' doesn't add watermark, 'both' outputs both versions.
182
183
  - `--max-pages-per-part`: Maximum number of pages per part for split translation. If not set, no splitting will be performed.
183
184
  - `--no-watermark`: [DEPRECATED] Use --watermark-output-mode=no_watermark instead.
185
+ - `--translate-table-text`: Translate table text (experimental, default: False)
184
186
 
185
187
  > [!TIP]
186
188
  > - Both `--skip-clean` and `--dual-translate-first` may help improve compatibility with some PDF readers
@@ -136,6 +136,7 @@ uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model
136
136
  - `--watermark-output-mode`: Control watermark output mode: 'watermarked' (default) adds watermark to translated PDF, 'no_watermark' doesn't add watermark, 'both' outputs both versions.
137
137
  - `--max-pages-per-part`: Maximum number of pages per part for split translation. If not set, no splitting will be performed.
138
138
  - `--no-watermark`: [DEPRECATED] Use --watermark-output-mode=no_watermark instead.
139
+ - `--translate-table-text`: Translate table text (experimental, default: False)
139
140
 
140
141
  > [!TIP]
141
142
  > - Both `--skip-clean` and `--dual-translate-first` may help improve compatibility with some PDF readers
@@ -0,0 +1 @@
1
+ __version__ = "0.2.15"
@@ -14,6 +14,10 @@ from babeldoc.assets.embedding_assets_metadata import (
14
14
  from babeldoc.assets.embedding_assets_metadata import EMBEDDING_FONT_METADATA
15
15
  from babeldoc.assets.embedding_assets_metadata import FONT_METADATA_URL
16
16
  from babeldoc.assets.embedding_assets_metadata import FONT_URL_BY_UPSTREAM
17
+ from babeldoc.assets.embedding_assets_metadata import (
18
+ TABLE_DETECTION_RAPIDOCR_MODEL_SHA3_256,
19
+ )
20
+ from babeldoc.assets.embedding_assets_metadata import TABLE_DETECTION_RAPIDOCR_MODEL_URL
17
21
  from babeldoc.assets.embedding_assets_metadata import TIKTOKEN_CACHES
18
22
  from babeldoc.const import get_cache_file_path
19
23
  from tenacity import retry
@@ -204,10 +208,36 @@ async def get_doclayout_onnx_model_path_async(client: httpx.AsyncClient | None =
204
208
  return onnx_path
205
209
 
206
210
 
211
+ async def get_table_detection_rapidocr_model_path_async(
212
+ client: httpx.AsyncClient | None = None,
213
+ ):
214
+ onnx_path = get_cache_file_path("ch_PP-OCRv4_det_infer.onnx", "models")
215
+ if verify_file(onnx_path, TABLE_DETECTION_RAPIDOCR_MODEL_SHA3_256):
216
+ return onnx_path
217
+
218
+ logger.info("table detection rapidocr model not found or corrupted, downloading...")
219
+ fastest_upstream, _ = await get_fastest_upstream_for_model(client)
220
+ if fastest_upstream is None:
221
+ logger.error("Failed to get fastest upstream")
222
+ exit(1)
223
+
224
+ url = TABLE_DETECTION_RAPIDOCR_MODEL_URL[fastest_upstream]
225
+
226
+ await download_file(client, url, onnx_path, TABLE_DETECTION_RAPIDOCR_MODEL_SHA3_256)
227
+ logger.info(
228
+ f"Download table detection rapidocr model from {fastest_upstream} success"
229
+ )
230
+ return onnx_path
231
+
232
+
207
233
  def get_doclayout_onnx_model_path():
208
234
  return run_coro(get_doclayout_onnx_model_path_async())
209
235
 
210
236
 
237
+ def get_table_detection_rapidocr_model_path():
238
+ return run_coro(get_table_detection_rapidocr_model_path_async())
239
+
240
+
211
241
  def get_font_url_by_name_and_upstream(font_file_name: str, upstream: str):
212
242
  if upstream not in FONT_URL_BY_UPSTREAM:
213
243
  logger.critical(f"Invalid upstream: {upstream}")
@@ -329,7 +359,11 @@ def generate_all_assets_file_list():
329
359
  {
330
360
  "name": "doclayout_yolo_docstructbench_imgsz1024.onnx",
331
361
  "sha3_256": DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256,
332
- }
362
+ },
363
+ {
364
+ "name": "ch_PP-OCRv4_det_infer.onnx",
365
+ "sha3_256": TABLE_DETECTION_RAPIDOCR_MODEL_SHA3_256,
366
+ },
333
367
  )
334
368
  return result
335
369
 
@@ -2,6 +2,10 @@ DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256 = (
2
2
  "60be061226930524958b5465c8c04af3d7c03bcb0beb66454f5da9f792e3cf2a"
3
3
  )
4
4
 
5
+ TABLE_DETECTION_RAPIDOCR_MODEL_SHA3_256 = (
6
+ "062f4619afe91b33147c033acadecbb53f2a7b99ac703d157b96d5b10948da5e"
7
+ )
8
+
5
9
  TIKTOKEN_CACHES = {
6
10
  "fb374d419588a4632f3f557e76b4b70aebbca790": "cb04bcda5782cfbbe77f2f991d92c0ea785d9496ef1137c91dfc3c8c324528d6"
7
11
  }
@@ -26,6 +30,12 @@ DOC_LAYOUT_ONNX_MODEL_URL = {
26
30
  "modelscope": "https://www.modelscope.cn/models/AI-ModelScope/DocLayout-YOLO-DocStructBench-onnx/resolve/master/doclayout_yolo_docstructbench_imgsz1024.onnx",
27
31
  }
28
32
 
33
+ TABLE_DETECTION_RAPIDOCR_MODEL_URL = {
34
+ "huggingface": "https://huggingface.co/spaces/RapidAI/RapidOCR/resolve/main/models/text_det/ch_PP-OCRv4_det_infer.onnx",
35
+ "hf-mirror": "https://hf-mirror.com/spaces/RapidAI/RapidOCR/resolve/main/models/text_det/ch_PP-OCRv4_det_infer.onnx",
36
+ "modelscope": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/master/onnx/PP-OCRv4/det/ch_PP-OCRv4_det_infer.onnx",
37
+ }
38
+
29
39
  # from https://github.com/funstory-ai/BabelDOC-Assets/blob/main/font_metadata.json
30
40
  EMBEDDING_FONT_METADATA = {
31
41
  "GoNotoKurrent-Bold.ttf": {
@@ -3,7 +3,7 @@ import shutil
3
3
  import subprocess
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.2.14"
6
+ __version__ = "0.2.15"
7
7
 
8
8
  CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"
9
9
 
@@ -287,6 +287,7 @@ class ParagraphFinder:
287
287
  "abandon",
288
288
  "figure_caption",
289
289
  "table_caption",
290
+ "table_text",
290
291
  ]
291
292
 
292
293
  def get_layout(
@@ -313,6 +314,7 @@ class ParagraphFinder:
313
314
  "table_footnote",
314
315
  "table_caption",
315
316
  "figure_caption",
317
+ "table_text",
316
318
  "table",
317
319
  "figure",
318
320
  "abandon",
@@ -0,0 +1,164 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import numpy as np
6
+ from pymupdf import Document
7
+
8
+ from babeldoc.document_il import il_version_1
9
+ from babeldoc.document_il.utils.style_helper import GREEN
10
+ from babeldoc.translation_config import TranslationConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class TableParser:
16
+ stage_name = "Parse Table"
17
+
18
+ def __init__(self, translation_config: TranslationConfig):
19
+ self.translation_config = translation_config
20
+ self.model = translation_config.table_model
21
+
22
+ def _save_debug_image(self, image: np.ndarray, layouts, page_number: int):
23
+ """Save debug image with drawn boxes if debug mode is enabled."""
24
+ if not self.translation_config.debug:
25
+ return
26
+
27
+ if not isinstance(layouts, list):
28
+ layouts = [layouts]
29
+ debug_dir = Path(
30
+ self.translation_config.get_working_file_path("table-ocr-box-image")
31
+ )
32
+ debug_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Draw boxes on the image
35
+ debug_image = image.copy()
36
+ for layout in layouts:
37
+ for box in layout.boxes:
38
+ x0, y0, x1, y1 = box.xyxy
39
+ cv2.rectangle(
40
+ debug_image,
41
+ (int(x0), int(y0)),
42
+ (int(x1), int(y1)),
43
+ (0, 255, 0),
44
+ 2,
45
+ )
46
+ # Add text label
47
+ cv2.putText(
48
+ debug_image,
49
+ layout.names[box.cls],
50
+ (int(x0), int(y0) - 5),
51
+ cv2.FONT_HERSHEY_SIMPLEX,
52
+ 0.5,
53
+ (0, 255, 0),
54
+ 1,
55
+ )
56
+
57
+ # Save the image
58
+ output_path = debug_dir / f"{page_number}.jpg"
59
+ cv2.imwrite(str(output_path), debug_image)
60
+
61
+ def _save_debug_box_to_page(self, page: il_version_1.Page):
62
+ """Save debug boxes and text labels to the PDF page."""
63
+ if not self.translation_config.debug:
64
+ return
65
+
66
+ color = GREEN
67
+
68
+ for layout in page.page_layout:
69
+ # Create a rectangle box
70
+ rect = il_version_1.PdfRectangle(
71
+ box=il_version_1.Box(
72
+ x=layout.box.x,
73
+ y=layout.box.y,
74
+ x2=layout.box.x2,
75
+ y2=layout.box.y2,
76
+ ),
77
+ graphic_state=color,
78
+ debug_info=True,
79
+ )
80
+ page.pdf_rectangle.append(rect)
81
+
82
+ # Create text label at top-left corner
83
+ # Note: PDF coordinates are from bottom-left,
84
+ # so we use y2 for top position
85
+ style = il_version_1.PdfStyle(
86
+ font_id="china-ss",
87
+ font_size=4,
88
+ graphic_state=color,
89
+ )
90
+ page.pdf_paragraph.append(
91
+ il_version_1.PdfParagraph(
92
+ first_line_indent=False,
93
+ box=il_version_1.Box(
94
+ x=layout.box.x,
95
+ y=layout.box.y2,
96
+ x2=layout.box.x2,
97
+ y2=layout.box.y2 + 5,
98
+ ),
99
+ vertical=False,
100
+ pdf_style=style,
101
+ unicode=layout.class_name,
102
+ pdf_paragraph_composition=[
103
+ il_version_1.PdfParagraphComposition(
104
+ pdf_same_style_unicode_characters=il_version_1.PdfSameStyleUnicodeCharacters(
105
+ unicode=layout.class_name,
106
+ pdf_style=style,
107
+ debug_info=True,
108
+ ),
109
+ ),
110
+ ],
111
+ xobj_id=-1,
112
+ ),
113
+ )
114
+
115
+ def process(self, docs: il_version_1.Document, mupdf_doc: Document):
116
+ """Generate layouts for all pages that need to be translated."""
117
+ # Get pages that need to be translated
118
+ have_table_pages = {}
119
+ for page in docs.page:
120
+ for layout in page.page_layout:
121
+ if layout.class_name == "table":
122
+ have_table_pages[page.page_number] = page
123
+ with self.translation_config.progress_monitor.stage_start(
124
+ self.stage_name,
125
+ len(have_table_pages),
126
+ ) as progress:
127
+ # Process predictions for each page
128
+ for page, layouts in self.model.handle_document(
129
+ have_table_pages.values(),
130
+ mupdf_doc,
131
+ self.translation_config,
132
+ self._save_debug_image,
133
+ ):
134
+ page_layouts = []
135
+ for layout in layouts.boxes:
136
+ # Convert coordinate system from picture to il
137
+ # system to the il coordinate system
138
+ x0, y0, x1, y1 = layout.xyxy
139
+ pix = mupdf_doc[page.page_number].get_pixmap()
140
+ h, w = pix.height, pix.width
141
+ x0, y0, x1, y1 = (
142
+ np.clip(int(x0 - 1), 0, w - 1),
143
+ np.clip(int(h - y1 - 1), 0, h - 1),
144
+ np.clip(int(x1 + 1), 0, w - 1),
145
+ np.clip(int(h - y0 + 1), 0, h - 1),
146
+ )
147
+ page_layout = il_version_1.PageLayout(
148
+ id=len(page_layouts) + 1,
149
+ box=il_version_1.Box(
150
+ x0.item(),
151
+ y0.item(),
152
+ x1.item(),
153
+ y1.item(),
154
+ ),
155
+ conf=layout.conf.item(),
156
+ class_name=layouts.names[layout.cls],
157
+ )
158
+ page_layouts.append(page_layout)
159
+
160
+ page.page_layout.extend(page_layouts)
161
+ self._save_debug_box_to_page(page)
162
+ progress.advance(1)
163
+
164
+ return docs
@@ -0,0 +1,316 @@
1
+ import logging
2
+ import re
3
+ from collections.abc import Generator
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from babeldoc.assets.assets import get_table_detection_rapidocr_model_path
8
+ from babeldoc.docvision.doclayout import YoloBox
9
+ from babeldoc.docvision.doclayout import YoloResult
10
+ from rapidocr_onnxruntime import RapidOCR
11
+
12
+ try:
13
+ import onnxruntime
14
+ except ImportError as e:
15
+ if "DLL load failed" in str(e):
16
+ raise OSError(
17
+ "Microsoft Visual C++ Redistributable is not installed. "
18
+ "Download it at https://aka.ms/vs/17/release/vc_redist.x64.exe"
19
+ ) from e
20
+ raise
21
+ import babeldoc.document_il.il_version_1
22
+ import pymupdf
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def convert_to_yolo_result(predictions):
28
+ """
29
+ Convert RapidOCR predictions to YoloResult format.
30
+
31
+ Args:
32
+ predictions (list): List of predictions, where each prediction is a list of coordinates
33
+ in format [[x1, y1], [x2, y2], [x3, y3], [x4, y4], (text, confidence)]
34
+ or a numpy array of format [x1, y1, x2, y2, ...]
35
+
36
+ Returns:
37
+ YoloResult: Converted predictions in YoloResult format
38
+ """
39
+ boxes = []
40
+
41
+ for pred in predictions:
42
+ # Check if the prediction is in the format of 4 corner points
43
+ if isinstance(pred, list) and len(pred) >= 5 and isinstance(pred[0], list):
44
+ # Convert 4 corner points to xyxy format (min x, min y, max x, max y)
45
+ points = np.array(pred[:4])
46
+ x1, y1 = points[:, 0].min(), points[:, 1].min()
47
+ x2, y2 = points[:, 0].max(), points[:, 1].max()
48
+ xyxy = [x1, y1, x2, y2]
49
+ box = YoloBox(xyxy=xyxy, conf=1.0, cls="text")
50
+ # Check if the prediction is already in xyxy format
51
+ elif isinstance(pred, list | np.ndarray) and len(pred) >= 4:
52
+ if isinstance(pred, np.ndarray):
53
+ pred = pred.tolist()
54
+ xyxy = pred[:4]
55
+ box = YoloBox(xyxy=xyxy, conf=1.0, cls="text")
56
+ else:
57
+ continue
58
+
59
+ boxes.append(box)
60
+
61
+ return YoloResult(names=["text"], boxes=boxes)
62
+
63
+
64
+ def create_yolo_result_from_nested_coords(nested_coords: np.ndarray, names: dict):
65
+ boxes = []
66
+
67
+ for quad in nested_coords.tolist():
68
+ if len(quad) != 4:
69
+ continue
70
+
71
+ # Convert quad coordinates to xyxy format (min x, min y, max x, max y)
72
+ x1, y1, x2, y2 = quad
73
+
74
+ # Create YoloBox with confidence 1.0 and class 'text'
75
+ box = YoloBox(
76
+ xyxy=[float(x1), float(y1), float(x2), float(y2)], conf=np.array(1.0), cls=0
77
+ )
78
+ boxes.append(box)
79
+
80
+ return YoloResult(names=names, boxes=boxes)
81
+
82
+
83
+ class RapidOCRModel:
84
+ def __init__(self):
85
+ self.use_cuda = False
86
+ self.use_dml = False
87
+ available_providers = onnxruntime.get_available_providers()
88
+ for provider in available_providers:
89
+ if re.match(r"dml", provider, re.IGNORECASE):
90
+ self.use_dml = True
91
+ elif re.match(r"cuda", provider, re.IGNORECASE):
92
+ self.use_cuda = True
93
+
94
+ self.model = RapidOCR(
95
+ det_model_path=get_table_detection_rapidocr_model_path(),
96
+ det_use_cuda=self.use_cuda,
97
+ det_use_dml=self.use_dml,
98
+ )
99
+ self.names = {0: "table_text"}
100
+
101
+ @property
102
+ def stride(self):
103
+ return 32
104
+
105
+ def resize_and_pad_image(self, image, new_shape):
106
+ """
107
+ Resize and pad the image to the specified size, ensuring dimensions are multiples of stride.
108
+
109
+ Parameters:
110
+ - image: Input image
111
+ - new_shape: Target size (integer or (height, width) tuple)
112
+ - stride: Padding alignment stride, default 32
113
+
114
+ Returns:
115
+ - Processed image
116
+ """
117
+ if isinstance(new_shape, int):
118
+ new_shape = (new_shape, new_shape)
119
+
120
+ h, w = image.shape[:2]
121
+ new_h, new_w = new_shape
122
+
123
+ # Calculate scaling ratio
124
+ r = min(new_h / h, new_w / w)
125
+ resized_h, resized_w = int(round(h * r)), int(round(w * r))
126
+
127
+ # Resize image
128
+ image = cv2.resize(
129
+ image,
130
+ (resized_w, resized_h),
131
+ interpolation=cv2.INTER_LINEAR,
132
+ )
133
+
134
+ # Calculate padding size and align to stride multiple
135
+ pad_w = (new_w - resized_w) % self.stride
136
+ pad_h = (new_h - resized_h) % self.stride
137
+ top, bottom = pad_h // 2, pad_h - pad_h // 2
138
+ left, right = pad_w // 2, pad_w - pad_w // 2
139
+
140
+ # Add padding
141
+ image = cv2.copyMakeBorder(
142
+ image,
143
+ top,
144
+ bottom,
145
+ left,
146
+ right,
147
+ cv2.BORDER_CONSTANT,
148
+ value=(114, 114, 114),
149
+ )
150
+
151
+ return image
152
+
153
+ def scale_boxes(self, img1_shape, boxes, img0_shape):
154
+ """
155
+ Rescales bounding boxes (in the format of xyxy by default) from the shape of the image they were originally
156
+ specified in (img1_shape) to the shape of a different image (img0_shape).
157
+
158
+ Args:
159
+ img1_shape (tuple): The shape of the image that the bounding boxes are for,
160
+ in the format of (height, width).
161
+ boxes (torch.Tensor): the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2)
162
+ img0_shape (tuple): the shape of the target image, in the format of (height, width).
163
+
164
+ Returns:
165
+ boxes (torch.Tensor): The scaled bounding boxes, in the format of (x1, y1, x2, y2)
166
+ """
167
+
168
+ # Calculate scaling ratio
169
+ gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
170
+
171
+ # Calculate padding size
172
+ pad_x = round((img1_shape[1] - img0_shape[1] * gain) / 2 - 0.1)
173
+ pad_y = round((img1_shape[0] - img0_shape[0] * gain) / 2 - 0.1)
174
+
175
+ # Remove padding and scale boxes
176
+ boxes[..., :4] = (boxes[..., :4] - [pad_x, pad_y, pad_x, pad_y]) / gain
177
+ return boxes
178
+
179
+ def predict(self, image, imgsz=800, batch_size=16, **kwargs):
180
+ """
181
+ Predict the layout of document pages.
182
+
183
+ Args:
184
+ image: A single image or a list of images of document pages.
185
+ imgsz: Resize the image to this size. Must be a multiple of the stride.
186
+ batch_size: Number of images to process in one batch.
187
+ **kwargs: Additional arguments.
188
+
189
+ Returns:
190
+ A YoloResult object containing the detected boxes.
191
+ """
192
+ # Handle single image input
193
+ assert isinstance(image, np.ndarray) and len(image.shape) == 3
194
+
195
+ # Calculate target size based on the maximum height in the batch
196
+ target_imgsz = 1024
197
+
198
+ orig_shape = (image.shape[0], image.shape[1])
199
+
200
+ pix = self.resize_and_pad_image(image, new_shape=target_imgsz)
201
+ # pix = np.transpose(pix, (2, 0, 1)) # CHW
202
+ # pix = pix.astype(np.float32) / 255.0 # Normalize to [0, 1]
203
+ input_ = pix
204
+
205
+ new_h, new_w = input_.shape[:2]
206
+
207
+ # Run inference
208
+ preds = self.model(input_, use_det=True, use_cls=False, use_rec=False)
209
+
210
+ # Process each prediction in the batch
211
+ if len(preds) > 0:
212
+ preds_np = np.array(preds[0])[:, [0, 2], :].reshape([-1, 4])
213
+ preds_np[..., :4] = self.scale_boxes(
214
+ (new_h, new_w),
215
+ preds_np[..., :4],
216
+ orig_shape,
217
+ )
218
+
219
+ # Convert predictions to YoloResult format
220
+ return create_yolo_result_from_nested_coords(preds_np, self.names)
221
+ else:
222
+ # Return empty YoloResult if no predictions
223
+ return YoloResult(names=self.names, boxes=[])
224
+
225
+ def handle_document(
226
+ self,
227
+ pages: list[babeldoc.document_il.il_version_1.Page],
228
+ mupdf_doc: pymupdf.Document,
229
+ translate_config,
230
+ save_debug_image,
231
+ ) -> Generator[
232
+ tuple[babeldoc.document_il.il_version_1.Page, YoloResult], None, None
233
+ ]:
234
+ for page in pages:
235
+ translate_config.raise_if_cancelled()
236
+ pix = mupdf_doc[page.page_number].get_pixmap(dpi=72)
237
+ image = np.fromstring(pix.samples, np.uint8).reshape(
238
+ pix.height,
239
+ pix.width,
240
+ 3,
241
+ )[:, :, ::-1]
242
+
243
+ table_boxes = []
244
+ for layout in page.page_layout:
245
+ if layout.class_name == "table":
246
+ table_boxes.append(layout.box)
247
+
248
+ predict_result = self.predict(image)
249
+
250
+ ok_boxes = []
251
+ for box in predict_result.boxes:
252
+ # Convert the box coordinates to float for proper comparison
253
+ box_xyxy = [float(coord) for coord in box.xyxy]
254
+
255
+ # Check if this box is inside any of the table boxes
256
+ for table_box in table_boxes:
257
+ # Determine if box is inside or overlapping with table_box with image dimensions
258
+ if self._is_box_in_table(
259
+ box_xyxy, table_box, page, image.shape[1], image.shape[0]
260
+ ):
261
+ ok_boxes.append(box)
262
+ break
263
+
264
+ yolo_result = YoloResult(names=self.names, boxes=ok_boxes)
265
+ save_debug_image(
266
+ image,
267
+ yolo_result,
268
+ page.page_number + 1,
269
+ )
270
+ yield page, yolo_result
271
+
272
+ def _is_box_in_table(self, box_xyxy, table_box, page, img_width, img_height):
273
+ """
274
+ Check if a box from image coordinates is inside a table box from PDF coordinates.
275
+
276
+ Args:
277
+ box_xyxy (list): Box coordinates in image coordinate system [x1, y1, x2, y2]
278
+ table_box (Box): Table box in PDF coordinate system
279
+ page: The page object containing information for coordinate conversion
280
+ img_width: Width of the image
281
+ img_height: Height of the image
282
+
283
+ Returns:
284
+ bool: True if the box is inside or significantly overlapping with the table box
285
+ """
286
+
287
+ # Get table box coordinates in PDF coordinate system
288
+ table_pdf_x1 = table_box.x
289
+ table_pdf_y1 = table_box.y
290
+ table_pdf_x2 = table_box.x2
291
+ table_pdf_y2 = table_box.y2
292
+
293
+ # Convert table box to image coordinates
294
+ table_img_x1 = table_pdf_x1
295
+ table_img_y1 = img_height - table_pdf_y2
296
+ table_img_x2 = table_pdf_x2
297
+ table_img_y2 = img_height - table_pdf_y1
298
+
299
+ # Now check for overlap between the boxes
300
+ # Calculate the area of overlap
301
+ x_overlap = max(
302
+ 0, min(box_xyxy[2], table_img_x2) - max(box_xyxy[0], table_img_x1)
303
+ )
304
+ y_overlap = max(
305
+ 0, min(box_xyxy[3], table_img_y2) - max(box_xyxy[1], table_img_y1)
306
+ )
307
+ overlap_area = x_overlap * y_overlap
308
+
309
+ # Calculate area of the detected box
310
+ box_area = (box_xyxy[2] - box_xyxy[0]) * (box_xyxy[3] - box_xyxy[1])
311
+
312
+ # If overlap area is significant relative to the box area, consider it inside
313
+ if box_area > 0 and overlap_area / box_area > 0.5:
314
+ return True
315
+
316
+ return False
@@ -34,6 +34,7 @@ from babeldoc.document_il.midend.il_translator_llm_only import ILTranslatorLLMOn
34
34
  from babeldoc.document_il.midend.layout_parser import LayoutParser
35
35
  from babeldoc.document_il.midend.paragraph_finder import ParagraphFinder
36
36
  from babeldoc.document_il.midend.styles_and_formulas import StylesAndFormulas
37
+ from babeldoc.document_il.midend.table_parser import TableParser
37
38
  from babeldoc.document_il.midend.typesetting import Typesetting
38
39
  from babeldoc.document_il.utils.fontmap import FontMapper
39
40
  from babeldoc.document_il.xml_converter import XMLConverter
@@ -51,6 +52,7 @@ TRANSLATE_STAGES = [
51
52
  (ILCreater.stage_name, 5.35), # Parse PDF and Create IR
52
53
  (DetectScannedFile.stage_name, 0.5), # No historical data, estimated
53
54
  (LayoutParser.stage_name, 6.42), # Parse Page Layout
55
+ (TableParser.stage_name, 0.5), # Parse Table
54
56
  (ParagraphFinder.stage_name, 2.14), # Parse Paragraphs
55
57
  (StylesAndFormulas.stage_name, 1.12), # Parse Formulas and Styles
56
58
  # (RemoveDescent.stage_name, 0.15), # Remove Char Descent
@@ -588,6 +590,15 @@ def _do_translate_single(
588
590
  docs,
589
591
  translation_config.get_working_file_path("layout_generator.json"),
590
592
  )
593
+
594
+ if translation_config.table_model:
595
+ docs = TableParser(translation_config).process(docs, doc_pdf2zh)
596
+ logger.debug("finish table parser")
597
+ if translation_config.debug:
598
+ xml_converter.write_json(
599
+ docs,
600
+ translation_config.get_working_file_path("table_parser.json"),
601
+ )
591
602
  ParagraphFinder(translation_config).process(docs)
592
603
  logger.debug(f"finish paragraph finder from {temp_pdf_path}")
593
604
  if translation_config.debug:
@@ -18,11 +18,12 @@ from babeldoc.document_il.translator.translator import OpenAITranslator
18
18
  from babeldoc.document_il.translator.translator import set_translate_rate_limiter
19
19
  from babeldoc.docvision.doclayout import DocLayoutModel
20
20
  from babeldoc.docvision.rpc_doclayout import RpcDocLayoutModel
21
+ from babeldoc.docvision.table_detection.rapidocr import RapidOCRModel
21
22
  from babeldoc.translation_config import TranslationConfig
22
23
  from babeldoc.translation_config import WatermarkOutputMode
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
- __version__ = "0.2.14"
26
+ __version__ = "0.2.15"
26
27
 
27
28
 
28
29
  def create_parser():
@@ -191,6 +192,12 @@ def create_parser():
191
192
  default=0.1,
192
193
  help="Progress report interval in seconds (default: 0.1)",
193
194
  )
195
+ translation_group.add_argument(
196
+ "--translate-table-text",
197
+ action="store_true",
198
+ default=False,
199
+ help="Translate table text (experimental)",
200
+ )
194
201
  # service option argument group
195
202
  service_group = translation_group.add_mutually_exclusive_group()
196
203
  service_group.add_argument(
@@ -276,6 +283,11 @@ async def main():
276
283
  else:
277
284
  doc_layout_model = DocLayoutModel.load_onnx()
278
285
 
286
+ if args.translate_table_text:
287
+ table_model = RapidOCRModel()
288
+ else:
289
+ table_model = None
290
+
279
291
  pending_files = []
280
292
  for file in args.files:
281
293
  # 清理文件路径,去除两端的引号
@@ -350,6 +362,7 @@ async def main():
350
362
  min_text_length=args.min_text_length,
351
363
  watermark_output_mode=watermark_output_mode,
352
364
  split_strategy=split_strategy,
365
+ table_model=table_model,
353
366
  )
354
367
 
355
368
  # Create progress handler
@@ -65,6 +65,7 @@ class TranslationConfig:
65
65
  watermark_output_mode: WatermarkOutputMode = WatermarkOutputMode.Watermarked,
66
66
  # Add split-related parameters
67
67
  split_strategy: BaseSplitStrategy | None = None,
68
+ table_model=None,
68
69
  ):
69
70
  self.translator = translator
70
71
 
@@ -142,6 +143,8 @@ class TranslationConfig:
142
143
  self._part_working_dirs: dict[int, Path] = {}
143
144
  self._part_output_dirs: dict[int, Path] = {}
144
145
 
146
+ self.table_model = table_model
147
+
145
148
  def parse_pages(self, pages_str: str | None) -> list[tuple[int, int]] | None:
146
149
  """解析页码字符串,返回页码范围列表
147
150
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "BabelDOC"
3
- version = "0.2.14"
3
+ version = "0.2.15"
4
4
  description = "Yet Another Document Translator"
5
5
  license = "AGPL-3.0"
6
6
  readme = "README.md"
@@ -42,6 +42,7 @@ dependencies = [
42
42
  "tiktoken>=0.9.0",
43
43
  "python-levenshtein>=0.27.1",
44
44
  "opencv-python-headless>=4.10.0.84",
45
+ "rapidocr-onnxruntime>=1.4.4",
45
46
  ]
46
47
 
47
48
  [project.optional-dependencies]
@@ -147,7 +148,7 @@ pythonpath = [".", "src"]
147
148
  testpaths = ["tests"]
148
149
 
149
150
  [bumpver]
150
- current_version = "0.2.14"
151
+ current_version = "0.2.15"
151
152
  version_pattern = "MAJOR.MINOR.PATCH[.PYTAGNUM]"
152
153
 
153
154
  [bumpver.file_patterns]
@@ -48,7 +48,7 @@ wheels = [
48
48
 
49
49
  [[package]]
50
50
  name = "babeldoc"
51
- version = "0.2.14"
51
+ version = "0.2.15"
52
52
  source = { editable = "." }
53
53
  dependencies = [
54
54
  { name = "bitstring" },
@@ -69,6 +69,7 @@ dependencies = [
69
69
  { name = "pydantic" },
70
70
  { name = "pymupdf" },
71
71
  { name = "python-levenshtein" },
72
+ { name = "rapidocr-onnxruntime" },
72
73
  { name = "rich" },
73
74
  { name = "scikit-image" },
74
75
  { name = "tenacity" },
@@ -123,6 +124,7 @@ requires-dist = [
123
124
  { name = "pydantic", specifier = ">=2.10.6" },
124
125
  { name = "pymupdf", specifier = ">=1.25.1" },
125
126
  { name = "python-levenshtein", specifier = ">=0.27.1" },
127
+ { name = "rapidocr-onnxruntime", specifier = ">=1.4.4" },
126
128
  { name = "rich", specifier = ">=13.9.4" },
127
129
  { name = "scikit-image", specifier = ">=0.25.2" },
128
130
  { name = "tenacity", specifier = ">=9.0.0" },
@@ -1416,6 +1418,23 @@ wheels = [
1416
1418
  { url = "https://files.pythonhosted.org/packages/c7/3b/1ba418920ecd1eae7cc4d4ac8a01711ee0879b1a57dd81d10551e5b9a2ea/openai-1.66.5-py3-none-any.whl", hash = "sha256:74be528175f8389f67675830c51a15bd51e874425c86d3de6153bf70ed6c2884", size = 571144 },
1417
1419
  ]
1418
1420
 
1421
+ [[package]]
1422
+ name = "opencv-python"
1423
+ version = "4.11.0.86"
1424
+ source = { registry = "https://pypi.org/simple" }
1425
+ dependencies = [
1426
+ { name = "numpy" },
1427
+ ]
1428
+ sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956 }
1429
+ wheels = [
1430
+ { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322 },
1431
+ { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197 },
1432
+ { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439 },
1433
+ { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597 },
1434
+ { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337 },
1435
+ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044 },
1436
+ ]
1437
+
1419
1438
  [[package]]
1420
1439
  name = "opencv-python-headless"
1421
1440
  version = "4.11.0.86"
@@ -1637,6 +1656,32 @@ wheels = [
1637
1656
  { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
1638
1657
  ]
1639
1658
 
1659
+ [[package]]
1660
+ name = "pyclipper"
1661
+ version = "1.3.0.post6"
1662
+ source = { registry = "https://pypi.org/simple" }
1663
+ sdist = { url = "https://files.pythonhosted.org/packages/4a/b2/550fe500e49c464d73fabcb8cb04d47e4885d6ca4cfc1f5b0a125a95b19a/pyclipper-1.3.0.post6.tar.gz", hash = "sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c", size = 165909 }
1664
+ wheels = [
1665
+ { url = "https://files.pythonhosted.org/packages/b5/34/0dca299fe41e9a92e78735502fed5238a4ac734755e624488df9b2eeec46/pyclipper-1.3.0.post6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa0f5e78cfa8262277bb3d0225537b3c2a90ef68fd90a229d5d24cf49955dcf4", size = 269504 },
1666
+ { url = "https://files.pythonhosted.org/packages/8a/5b/81528b08134b3c2abdfae821e1eff975c0703802d41974b02dfb2e101c55/pyclipper-1.3.0.post6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a01f182d8938c1dc515e8508ed2442f7eebd2c25c7d5cb29281f583c1a8008a4", size = 142599 },
1667
+ { url = "https://files.pythonhosted.org/packages/84/a4/3e304f6c0d000382cd54d4a1e5f0d8fc28e1ae97413a2ec1016a7b840319/pyclipper-1.3.0.post6-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:640f20975727994d4abacd07396f564e9e5665ba5cb66ceb36b300c281f84fa4", size = 912209 },
1668
+ { url = "https://files.pythonhosted.org/packages/f5/6a/28ec55cc3f972368b211fca017e081cf5a71009d1b8ec3559767cda5b289/pyclipper-1.3.0.post6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63002f6bb0f1efa87c0b81634cbb571066f237067e23707dabf746306c92ba5", size = 929511 },
1669
+ { url = "https://files.pythonhosted.org/packages/c4/56/c326f3454c5f30a31f58a5c3154d891fce58ad73ccbf1d3f4aacfcbd344d/pyclipper-1.3.0.post6-cp310-cp310-win32.whl", hash = "sha256:106b8622cd9fb07d80cbf9b1d752334c55839203bae962376a8c59087788af26", size = 100126 },
1670
+ { url = "https://files.pythonhosted.org/packages/f8/e6/f8239af6346848b20a3448c554782fe59298ab06c1d040490242dc7e3c26/pyclipper-1.3.0.post6-cp310-cp310-win_amd64.whl", hash = "sha256:9699e98862dadefd0bea2360c31fa61ca553c660cbf6fb44993acde1b959f58f", size = 110470 },
1671
+ { url = "https://files.pythonhosted.org/packages/50/a9/66ca5f252dcac93ca076698591b838ba17f9729591edf4b74fef7fbe1414/pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef", size = 270930 },
1672
+ { url = "https://files.pythonhosted.org/packages/59/fe/2ab5818b3504e179086e54a37ecc245525d069267b8c31b18ec3d0830cbf/pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa", size = 143411 },
1673
+ { url = "https://files.pythonhosted.org/packages/09/f7/b58794f643e033a6d14da7c70f517315c3072f3c5fccdf4232fa8c8090c1/pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0", size = 951754 },
1674
+ { url = "https://files.pythonhosted.org/packages/c1/77/846a21957cd4ed266c36705ee340beaa923eb57d2bba013cfd7a5c417cfd/pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace1f0753cf71c5c5f6488b8feef5dd0fa8b976ad86b24bb51f708f513df4aac", size = 969608 },
1675
+ { url = "https://files.pythonhosted.org/packages/c9/2b/580703daa6606d160caf596522d4cfdf62ae619b062a7ce6f905821a57e8/pyclipper-1.3.0.post6-cp311-cp311-win32.whl", hash = "sha256:dbc828641667142751b1127fd5c4291663490cf05689c85be4c5bcc89aaa236a", size = 100227 },
1676
+ { url = "https://files.pythonhosted.org/packages/17/4b/a4cda18e8556d913ff75052585eb0d658500596b5f97fe8401d05123d47b/pyclipper-1.3.0.post6-cp311-cp311-win_amd64.whl", hash = "sha256:1c03f1ae43b18ee07730c3c774cc3cf88a10c12a4b097239b33365ec24a0a14a", size = 110442 },
1677
+ { url = "https://files.pythonhosted.org/packages/fc/c8/197d9a1d8354922d24d11d22fb2e0cc1ebc182f8a30496b7ddbe89467ce1/pyclipper-1.3.0.post6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6363b9d79ba1b5d8f32d1623e797c1e9f994600943402e68d5266067bdde173e", size = 270487 },
1678
+ { url = "https://files.pythonhosted.org/packages/8e/8e/eb14eadf054494ad81446e21c4ea163b941747610b0eb9051644395f567e/pyclipper-1.3.0.post6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32cd7fb9c1c893eb87f82a072dbb5e26224ea7cebbad9dc306d67e1ac62dd229", size = 143469 },
1679
+ { url = "https://files.pythonhosted.org/packages/cf/e5/6c4a8df6e904c133bb4c5309d211d31c751db60cbd36a7250c02b05494a1/pyclipper-1.3.0.post6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3aab10e3c10ed8fa60c608fb87c040089b83325c937f98f06450cf9fcfdaf1d", size = 944206 },
1680
+ { url = "https://files.pythonhosted.org/packages/76/65/cb014acc41cd5bf6bbfa4671c7faffffb9cee01706642c2dec70c5209ac8/pyclipper-1.3.0.post6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c", size = 963797 },
1681
+ { url = "https://files.pythonhosted.org/packages/80/ec/b40cd81ab7598984167508a5369a2fa31a09fe3b3e3d0b73aa50e06d4b3f/pyclipper-1.3.0.post6-cp312-cp312-win32.whl", hash = "sha256:793b0aa54b914257aa7dc76b793dd4dcfb3c84011d48df7e41ba02b571616eaf", size = 99456 },
1682
+ { url = "https://files.pythonhosted.org/packages/24/3a/7d6292e3c94fb6b872d8d7e80d909dc527ee6b0af73b753c63fdde65a7da/pyclipper-1.3.0.post6-cp312-cp312-win_amd64.whl", hash = "sha256:d3f9da96f83b8892504923beb21a481cd4516c19be1d39eb57a92ef1c9a29548", size = 110278 },
1683
+ ]
1684
+
1640
1685
  [[package]]
1641
1686
  name = "pycparser"
1642
1687
  version = "2.22"
@@ -1929,6 +1974,25 @@ wheels = [
1929
1974
  { url = "https://files.pythonhosted.org/packages/09/f6/fa777f336629aee8938f3d5c95c09df38459d4eadbdbe34642889857fb6a/rapidfuzz-3.12.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:54bb69ebe5ca0bd7527357e348f16a4c0c52fe0c2fcc8a041010467dcb8385f7", size = 1555000 },
1930
1975
  ]
1931
1976
 
1977
+ [[package]]
1978
+ name = "rapidocr-onnxruntime"
1979
+ version = "1.4.4"
1980
+ source = { registry = "https://pypi.org/simple" }
1981
+ dependencies = [
1982
+ { name = "numpy" },
1983
+ { name = "onnxruntime" },
1984
+ { name = "opencv-python" },
1985
+ { name = "pillow" },
1986
+ { name = "pyclipper" },
1987
+ { name = "pyyaml" },
1988
+ { name = "shapely" },
1989
+ { name = "six" },
1990
+ { name = "tqdm" },
1991
+ ]
1992
+ wheels = [
1993
+ { url = "https://files.pythonhosted.org/packages/ba/12/1e5497183bdbe782dbb91bad1d0d2297dba4d2831b2652657f7517bfc6df/rapidocr_onnxruntime-1.4.4-py3-none-any.whl", hash = "sha256:971d7d5f223a7a808662229df1ef69893809d8457d834e6373d3854bc1782cbf", size = 14915192 },
1994
+ ]
1995
+
1932
1996
  [[package]]
1933
1997
  name = "regex"
1934
1998
  version = "2024.11.6"
@@ -2108,6 +2172,35 @@ wheels = [
2108
2172
  { url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317 },
2109
2173
  ]
2110
2174
 
2175
+ [[package]]
2176
+ name = "shapely"
2177
+ version = "2.0.7"
2178
+ source = { registry = "https://pypi.org/simple" }
2179
+ dependencies = [
2180
+ { name = "numpy" },
2181
+ ]
2182
+ sdist = { url = "https://files.pythonhosted.org/packages/21/c0/a911d1fd765d07a2b6769ce155219a281bfbe311584ebe97340d75c5bdb1/shapely-2.0.7.tar.gz", hash = "sha256:28fe2997aab9a9dc026dc6a355d04e85841546b2a5d232ed953e3321ab958ee5", size = 283413 }
2183
+ wheels = [
2184
+ { url = "https://files.pythonhosted.org/packages/15/2e/02c694d6ddacd4f13b625722d313d2838f23c5b988cbc680132983f73ce3/shapely-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:33fb10e50b16113714ae40adccf7670379e9ccf5b7a41d0002046ba2b8f0f691", size = 1478310 },
2185
+ { url = "https://files.pythonhosted.org/packages/87/69/b54a08bcd25e561bdd5183c008ace4424c25e80506e80674032504800efd/shapely-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f44eda8bd7a4bccb0f281264b34bf3518d8c4c9a8ffe69a1a05dabf6e8461147", size = 1336082 },
2186
+ { url = "https://files.pythonhosted.org/packages/b3/f9/40473fcb5b66ff849e563ca523d2a26dafd6957d52dd876ffd0eded39f1c/shapely-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6c50cd879831955ac47af9c907ce0310245f9d162e298703f82e1785e38c98", size = 2371047 },
2187
+ { url = "https://files.pythonhosted.org/packages/d6/f3/c9cc07a7a03b5f5e83bd059f9adf3e21cf086b0e41d7f95e6464b151e798/shapely-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04a65d882456e13c8b417562c36324c0cd1e5915f3c18ad516bb32ee3f5fc895", size = 2469112 },
2188
+ { url = "https://files.pythonhosted.org/packages/5d/b9/fc63d6b0b25063a3ff806857a5dc88851d54d1c278288f18cef1b322b449/shapely-2.0.7-cp310-cp310-win32.whl", hash = "sha256:7e97104d28e60b69f9b6a957c4d3a2a893b27525bc1fc96b47b3ccef46726bf2", size = 1296057 },
2189
+ { url = "https://files.pythonhosted.org/packages/fe/d1/8df43f94cf4cda0edbab4545f7cdd67d3f1d02910eaff152f9f45c6d00d8/shapely-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:35524cc8d40ee4752520819f9894b9f28ba339a42d4922e92c99b148bed3be39", size = 1441787 },
2190
+ { url = "https://files.pythonhosted.org/packages/1d/ad/21798c2fec013e289f8ab91d42d4d3299c315b8c4460c08c75fef0901713/shapely-2.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5cf23400cb25deccf48c56a7cdda8197ae66c0e9097fcdd122ac2007e320bc34", size = 1473091 },
2191
+ { url = "https://files.pythonhosted.org/packages/15/63/eef4f180f1b5859c70e7f91d2f2570643e5c61e7d7c40743d15f8c6cbc42/shapely-2.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f1da01c04527f7da59ee3755d8ee112cd8967c15fab9e43bba936b81e2a013", size = 1332921 },
2192
+ { url = "https://files.pythonhosted.org/packages/fe/67/77851dd17738bbe7762a0ef1acf7bc499d756f68600dd68a987d78229412/shapely-2.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f623b64bb219d62014781120f47499a7adc30cf7787e24b659e56651ceebcb0", size = 2427949 },
2193
+ { url = "https://files.pythonhosted.org/packages/0b/a5/2c8dbb0f383519771df19164e3bf3a8895d195d2edeab4b6040f176ee28e/shapely-2.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6d95703efaa64aaabf278ced641b888fc23d9c6dd71f8215091afd8a26a66e3", size = 2529282 },
2194
+ { url = "https://files.pythonhosted.org/packages/dc/4e/e1d608773c7fe4cde36d48903c0d6298e3233dc69412403783ac03fa5205/shapely-2.0.7-cp311-cp311-win32.whl", hash = "sha256:2f6e4759cf680a0f00a54234902415f2fa5fe02f6b05546c662654001f0793a2", size = 1295751 },
2195
+ { url = "https://files.pythonhosted.org/packages/27/57/8ec7c62012bed06731f7ee979da7f207bbc4b27feed5f36680b6a70df54f/shapely-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:b52f3ab845d32dfd20afba86675c91919a622f4627182daec64974db9b0b4608", size = 1442684 },
2196
+ { url = "https://files.pythonhosted.org/packages/4f/3e/ea100eec5811bafd0175eb21828a3be5b0960f65250f4474391868be7c0f/shapely-2.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4c2b9859424facbafa54f4a19b625a752ff958ab49e01bc695f254f7db1835fa", size = 1482451 },
2197
+ { url = "https://files.pythonhosted.org/packages/ce/53/c6a3487716fd32e1f813d2a9608ba7b72a8a52a6966e31c6443480a1d016/shapely-2.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5aed1c6764f51011d69a679fdf6b57e691371ae49ebe28c3edb5486537ffbd51", size = 1345765 },
2198
+ { url = "https://files.pythonhosted.org/packages/fd/dd/b35d7891d25cc11066a70fb8d8169a6a7fca0735dd9b4d563a84684969a3/shapely-2.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c9ae8cf443187d784d57202199bf9fd2d4bb7d5521fe8926ba40db1bc33e8e", size = 2421540 },
2199
+ { url = "https://files.pythonhosted.org/packages/62/de/8dbd7df60eb23cb983bb698aac982944b3d602ef0ce877a940c269eae34e/shapely-2.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9469f49ff873ef566864cb3516091881f217b5d231c8164f7883990eec88b73", size = 2525741 },
2200
+ { url = "https://files.pythonhosted.org/packages/96/64/faf0413ebc7a84fe7a0790bf39ec0b02b40132b68e57aba985c0b6e4e7b6/shapely-2.0.7-cp312-cp312-win32.whl", hash = "sha256:6bca5095e86be9d4ef3cb52d56bdd66df63ff111d580855cb8546f06c3c907cd", size = 1296552 },
2201
+ { url = "https://files.pythonhosted.org/packages/63/05/8a1c279c226d6ad7604d9e237713dd21788eab96db97bf4ce0ea565e5596/shapely-2.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f86e2c0259fe598c4532acfcf638c1f520fa77c1275912bbc958faecbf00b108", size = 1443464 },
2202
+ ]
2203
+
2111
2204
  [[package]]
2112
2205
  name = "six"
2113
2206
  version = "1.17.0"
@@ -1 +0,0 @@
1
- __version__ = "0.2.14"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes