BabelDOC 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. babeldoc/__init__.py +1 -0
  2. babeldoc/asynchronize/__init__.py +51 -0
  3. babeldoc/const.py +7 -0
  4. babeldoc/converter.py +510 -0
  5. babeldoc/document_il/__init__.py +43 -0
  6. babeldoc/document_il/backend/__init__.py +0 -0
  7. babeldoc/document_il/backend/pdf_creater.py +402 -0
  8. babeldoc/document_il/frontend/__init__.py +0 -0
  9. babeldoc/document_il/frontend/il_creater.py +348 -0
  10. babeldoc/document_il/il_version_1.py +799 -0
  11. babeldoc/document_il/il_version_1.rnc +141 -0
  12. babeldoc/document_il/il_version_1.rng +390 -0
  13. babeldoc/document_il/il_version_1.xsd +235 -0
  14. babeldoc/document_il/midend/__init__.py +0 -0
  15. babeldoc/document_il/midend/add_debug_information.py +138 -0
  16. babeldoc/document_il/midend/il_translator.py +553 -0
  17. babeldoc/document_il/midend/layout_parser.py +180 -0
  18. babeldoc/document_il/midend/paragraph_finder.py +452 -0
  19. babeldoc/document_il/midend/remove_descent.py +168 -0
  20. babeldoc/document_il/midend/styles_and_formulas.py +701 -0
  21. babeldoc/document_il/midend/typesetting.py +761 -0
  22. babeldoc/document_il/translator/__init__.py +0 -0
  23. babeldoc/document_il/translator/cache.py +145 -0
  24. babeldoc/document_il/translator/translator.py +267 -0
  25. babeldoc/document_il/utils/fontmap.py +227 -0
  26. babeldoc/document_il/utils/layout_helper.py +467 -0
  27. babeldoc/document_il/utils/style_helper.py +86 -0
  28. babeldoc/document_il/xml_converter.py +51 -0
  29. babeldoc/docvision/README.md +0 -0
  30. babeldoc/docvision/__init__.py +0 -0
  31. babeldoc/docvision/doclayout.py +278 -0
  32. babeldoc/docvision/rpc_doclayout.py +162 -0
  33. babeldoc/high_level.py +468 -0
  34. babeldoc/main.py +466 -0
  35. babeldoc/pdfinterp.py +464 -0
  36. babeldoc/progress_monitor.py +146 -0
  37. babeldoc/translation_config.py +156 -0
  38. babeldoc-0.1.6.dist-info/METADATA +283 -0
  39. babeldoc-0.1.6.dist-info/RECORD +42 -0
  40. babeldoc-0.1.6.dist-info/WHEEL +4 -0
  41. babeldoc-0.1.6.dist-info/entry_points.txt +2 -0
  42. babeldoc-0.1.6.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,278 @@
1
+ import abc
2
+ import ast
3
+ import os.path
4
+ import platform
5
+ from pathlib import Path
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import onnx
10
+ import onnxruntime
11
+ from huggingface_hub import hf_hub_download
12
+
13
+
14
+ class DocLayoutModel(abc.ABC):
15
+ @staticmethod
16
+ def load_onnx():
17
+ model = OnnxModel.from_pretrained(
18
+ repo_id="wybxc/DocLayout-YOLO-DocStructBench-onnx",
19
+ filename="doclayout_yolo_docstructbench_imgsz1024.onnx",
20
+ )
21
+ return model
22
+
23
+ @staticmethod
24
+ def load_available():
25
+ return DocLayoutModel.load_onnx()
26
+
27
+ @property
28
+ @abc.abstractmethod
29
+ def stride(self) -> int:
30
+ """Stride of the model input."""
31
+ pass
32
+
33
+ @abc.abstractmethod
34
+ def predict(self, image, imgsz=1024, **kwargs) -> list:
35
+ """
36
+ Predict the layout of a document page.
37
+
38
+ Args:
39
+ image: The image of the document page.
40
+ imgsz: Resize the image to this size. Must be a multiple of the stride.
41
+ **kwargs: Additional arguments.
42
+ """
43
+ pass
44
+
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
57
+
58
+
59
+ class YoloBox:
60
+ """Helper class to store detection results from ONNX model."""
61
+
62
+ def __init__(self, data=None, xyxy=None, conf=None, cls=None):
63
+ if data is not None:
64
+ self.xyxy = data[:4]
65
+ self.conf = data[-2]
66
+ self.cls = data[-1]
67
+ return
68
+ assert xyxy is not None and conf is not None and cls is not None
69
+ self.xyxy = xyxy
70
+ self.conf = conf
71
+ self.cls = cls
72
+
73
+
74
+ # 检测操作系统类型
75
+ os_name = platform.system()
76
+
77
+ providers = []
78
+
79
+ if os_name == "Darwin": # Temporarily disable CoreML due to batch inference issues
80
+ providers.append(
81
+ (
82
+ "CoreMLExecutionProvider",
83
+ {
84
+ "ModelFormat": "MLProgram",
85
+ "MLComputeUnits": "ALL",
86
+ "RequireStaticInputShapes": "0",
87
+ "EnableOnSubgraphs": "0",
88
+ },
89
+ ),
90
+ )
91
+ # workaround for CoreML batch inference issues
92
+ max_batch_size = 1
93
+ else:
94
+ max_batch_size = 1024
95
+ providers.append("CPUExecutionProvider") # CPU执行提供者作为通用后备选项
96
+
97
+
98
+ class OnnxModel(DocLayoutModel):
99
+ def __init__(self, model_path: str):
100
+ self.model_path = model_path
101
+
102
+ model = onnx.load(model_path)
103
+ metadata = {d.key: d.value for d in model.metadata_props}
104
+ self._stride = ast.literal_eval(metadata["stride"])
105
+ self._names = ast.literal_eval(metadata["names"])
106
+
107
+ self.model = onnxruntime.InferenceSession(
108
+ model.SerializeToString(),
109
+ providers=providers,
110
+ )
111
+
112
+ @staticmethod
113
+ def from_pretrained(repo_id: str, filename: str):
114
+ if os.environ.get("USE_MODELSCOPE", "0") == "1":
115
+ repo_mapping = {
116
+ # Edit here to add more models
117
+ "wybxc/DocLayout-YOLO-DocStructBench-onnx": "AI-ModelScope/DocLayout-YOLO-DocStructBench-onnx",
118
+ }
119
+ from modelscope import snapshot_download
120
+
121
+ model_dir = snapshot_download(repo_mapping[repo_id])
122
+ pth = Path(model_dir) / filename
123
+ else:
124
+ try:
125
+ pth = hf_hub_download(
126
+ repo_id=repo_id,
127
+ filename=filename,
128
+ etag_timeout=1,
129
+ local_files_only=True,
130
+ )
131
+ except Exception:
132
+ pth = hf_hub_download(
133
+ repo_id=repo_id,
134
+ filename=filename,
135
+ etag_timeout=1,
136
+ )
137
+ return OnnxModel(pth)
138
+
139
+ @property
140
+ def stride(self):
141
+ return self._stride
142
+
143
+ def resize_and_pad_image(self, image, new_shape):
144
+ """
145
+ Resize and pad the image to the specified size, ensuring dimensions are multiples of stride.
146
+
147
+ Parameters:
148
+ - image: Input image
149
+ - new_shape: Target size (integer or (height, width) tuple)
150
+ - stride: Padding alignment stride, default 32
151
+
152
+ Returns:
153
+ - Processed image
154
+ """
155
+ if isinstance(new_shape, int):
156
+ new_shape = (new_shape, new_shape)
157
+
158
+ h, w = image.shape[:2]
159
+ new_h, new_w = new_shape
160
+
161
+ # Calculate scaling ratio
162
+ r = min(new_h / h, new_w / w)
163
+ resized_h, resized_w = int(round(h * r)), int(round(w * r))
164
+
165
+ # Resize image
166
+ image = cv2.resize(
167
+ image,
168
+ (resized_w, resized_h),
169
+ interpolation=cv2.INTER_LINEAR,
170
+ )
171
+
172
+ # Calculate padding size and align to stride multiple
173
+ pad_w = (new_w - resized_w) % self.stride
174
+ pad_h = (new_h - resized_h) % self.stride
175
+ top, bottom = pad_h // 2, pad_h - pad_h // 2
176
+ left, right = pad_w // 2, pad_w - pad_w // 2
177
+
178
+ # Add padding
179
+ image = cv2.copyMakeBorder(
180
+ image,
181
+ top,
182
+ bottom,
183
+ left,
184
+ right,
185
+ cv2.BORDER_CONSTANT,
186
+ value=(114, 114, 114),
187
+ )
188
+
189
+ return image
190
+
191
+ def scale_boxes(self, img1_shape, boxes, img0_shape):
192
+ """
193
+ Rescales bounding boxes (in the format of xyxy by default) from the shape of the image they were originally
194
+ specified in (img1_shape) to the shape of a different image (img0_shape).
195
+
196
+ Args:
197
+ img1_shape (tuple): The shape of the image that the bounding boxes are for,
198
+ in the format of (height, width).
199
+ boxes (torch.Tensor): the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2)
200
+ img0_shape (tuple): the shape of the target image, in the format of (height, width).
201
+
202
+ Returns:
203
+ boxes (torch.Tensor): The scaled bounding boxes, in the format of (x1, y1, x2, y2)
204
+ """
205
+
206
+ # Calculate scaling ratio
207
+ gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
208
+
209
+ # Calculate padding size
210
+ pad_x = round((img1_shape[1] - img0_shape[1] * gain) / 2 - 0.1)
211
+ pad_y = round((img1_shape[0] - img0_shape[0] * gain) / 2 - 0.1)
212
+
213
+ # Remove padding and scale boxes
214
+ boxes[..., :4] = (boxes[..., :4] - [pad_x, pad_y, pad_x, pad_y]) / gain
215
+ return boxes
216
+
217
+ def predict(self, image, imgsz=1024, batch_size=16, **kwargs):
218
+ """
219
+ Predict the layout of document pages.
220
+
221
+ Args:
222
+ image: A single image or a list of images of document pages.
223
+ imgsz: Resize the image to this size. Must be a multiple of the stride.
224
+ batch_size: Number of images to process in one batch.
225
+ **kwargs: Additional arguments.
226
+
227
+ Returns:
228
+ A list of YoloResult objects, one for each input image.
229
+ """
230
+ # Handle single image input
231
+ if isinstance(image, np.ndarray) and len(image.shape) == 3:
232
+ image = [image]
233
+
234
+ total_images = len(image)
235
+ results = []
236
+ batch_size = min(batch_size, max_batch_size)
237
+
238
+ # Process images in batches
239
+ for i in range(0, total_images, batch_size):
240
+ batch_images = image[i : i + batch_size]
241
+ batch_size_actual = len(batch_images)
242
+
243
+ # Calculate target size based on the maximum height in the batch
244
+ max_height = max(img.shape[0] for img in batch_images)
245
+ target_imgsz = int(max_height / 32) * 32
246
+
247
+ # Preprocess batch
248
+ processed_batch = []
249
+ orig_shapes = []
250
+ for img in batch_images:
251
+ orig_h, orig_w = img.shape[:2]
252
+ orig_shapes.append((orig_h, orig_w))
253
+
254
+ pix = self.resize_and_pad_image(img, new_shape=target_imgsz)
255
+ pix = np.transpose(pix, (2, 0, 1)) # CHW
256
+ pix = pix.astype(np.float32) / 255.0 # Normalize to [0, 1]
257
+ processed_batch.append(pix)
258
+
259
+ # Stack batch
260
+ batch_input = np.stack(processed_batch, axis=0) # BCHW
261
+ new_h, new_w = batch_input.shape[2:]
262
+
263
+ # Run inference
264
+ batch_preds = self.model.run(None, {"images": batch_input})[0]
265
+
266
+ # Process each prediction in the batch
267
+ for j in range(batch_size_actual):
268
+ preds = batch_preds[j]
269
+ preds = preds[preds[..., 4] > 0.25]
270
+ if len(preds) > 0:
271
+ preds[..., :4] = self.scale_boxes(
272
+ (new_h, new_w),
273
+ preds[..., :4],
274
+ orig_shapes[j],
275
+ )
276
+ results.append(YoloResult(boxes_data=preds, names=self._names))
277
+
278
+ return results
@@ -0,0 +1,162 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import httpx
6
+ import msgpack
7
+ import numpy as np
8
+
9
+ from babeldoc.docvision.doclayout import DocLayoutModel
10
+ from babeldoc.docvision.doclayout import YoloBox
11
+ from babeldoc.docvision.doclayout import YoloResult
12
+
13
+ # 设置日志
14
+ logging.basicConfig(level=logging.DEBUG)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def encode_image(image) -> bytes:
19
+ """Read and encode image to bytes
20
+
21
+ Args:
22
+ image: Can be either a file path (str) or numpy array
23
+ """
24
+ if isinstance(image, str):
25
+ if not Path(image).exists():
26
+ raise FileNotFoundError(f"Image file not found: {image}")
27
+ img = cv2.imread(image)
28
+ if img is None:
29
+ raise ValueError(f"Failed to read image: {image}")
30
+ else:
31
+ img = image
32
+
33
+ # logger.debug(f"Image shape: {img.shape}")
34
+ encoded = cv2.imencode(".jpg", img)[1].tobytes()
35
+ # logger.debug(f"Encoded image size: {len(encoded)} bytes")
36
+ return encoded
37
+
38
+
39
+ def predict_layout(
40
+ image,
41
+ host: str = "http://localhost:8000",
42
+ imgsz: int = 1024,
43
+ ):
44
+ """
45
+ Predict document layout using the MOSEC service
46
+
47
+ Args:
48
+ image: Can be either a file path (str) or numpy array
49
+ host: Service host URL
50
+ imgsz: Image size for model input
51
+
52
+ Returns:
53
+ List of predictions containing bounding boxes and classes
54
+ """
55
+ try:
56
+ # Prepare request data
57
+ if not isinstance(image, list):
58
+ image = [image]
59
+ image_data = [encode_image(image) for image in image]
60
+ data = {
61
+ "image": image_data,
62
+ "imgsz": imgsz,
63
+ }
64
+
65
+ # Pack data using msgpack
66
+ packed_data = msgpack.packb(data, use_bin_type=True)
67
+ logger.debug(f"Packed data size: {len(packed_data)} bytes")
68
+
69
+ # Send request
70
+ logger.debug(f"Sending request to {host}/inference")
71
+ response = httpx.post(
72
+ f"{host}/inference",
73
+ data=packed_data,
74
+ headers={
75
+ "Content-Type": "application/msgpack",
76
+ "Accept": "application/msgpack",
77
+ },
78
+ timeout=30,
79
+ )
80
+
81
+ logger.debug(f"Response status: {response.status_code}")
82
+ logger.debug(f"Response headers: {response.headers}")
83
+
84
+ if response.status_code == 200:
85
+ try:
86
+ result = msgpack.unpackb(response.content, raw=False)
87
+ return result
88
+ except Exception as e:
89
+ logger.exception(f"Failed to unpack response: {e!s}")
90
+ raise
91
+ else:
92
+ logger.error(f"Request failed with status {response.status_code}")
93
+ logger.error(f"Response content: {response.content}")
94
+ raise Exception(
95
+ f"Request failed with status {response.status_code}: {response.text}",
96
+ )
97
+ except Exception as e:
98
+ logger.exception(f"Unexpected error: {e!s}")
99
+ raise
100
+
101
+
102
+ class RpcDocLayoutModel(DocLayoutModel):
103
+ """DocLayoutModel implementation that uses RPC service."""
104
+
105
+ def __init__(self, host: str = "http://localhost:8000"):
106
+ """Initialize RPC model with host address."""
107
+ self.host = host
108
+ self._stride = 32 # Default stride value
109
+ self._names = ["text", "title", "list", "table", "figure"]
110
+
111
+ @property
112
+ def stride(self) -> int:
113
+ """Stride of the model input."""
114
+ return self._stride
115
+
116
+ def predict(self, image, imgsz=1024, **kwargs) -> list[YoloResult]:
117
+ """Predict the layout of document pages using RPC service."""
118
+ # Handle single image input
119
+ if isinstance(image, np.ndarray) and len(image.shape) == 3:
120
+ image = [image]
121
+
122
+ results = []
123
+ preds = predict_layout(image, host=self.host, imgsz=imgsz)
124
+ if len(preds) > 0:
125
+ for pred in preds:
126
+ boxes = [
127
+ YoloBox(None, np.array(x["xyxy"]), np.array(x["conf"]), x["cls"])
128
+ for x in pred["boxes"]
129
+ ]
130
+ results.append(
131
+ YoloResult(
132
+ boxes=boxes,
133
+ names={int(k): v for k, v in pred["names"].items()},
134
+ ),
135
+ )
136
+ else:
137
+ results.append(YoloResult(boxes_data=np.array([]), names=[]))
138
+
139
+ return results
140
+
141
+ @staticmethod
142
+ def from_host(host: str) -> "RpcDocLayoutModel":
143
+ """Create RpcDocLayoutModel from host address."""
144
+ return RpcDocLayoutModel(host=host)
145
+
146
+
147
+ if __name__ == "__main__":
148
+ # Test the service
149
+ try:
150
+ # Use a default test image if example/1.png doesn't exist
151
+ image_path = "example/1.png"
152
+ if not Path(image_path).exists():
153
+ print(f"Warning: {image_path} not found.")
154
+ print("Please provide the path to a test image:")
155
+ image_path = input("> ")
156
+
157
+ logger.info(f"Processing image: {image_path}")
158
+ result = predict_layout(image_path)
159
+ print("Prediction results:")
160
+ print(result)
161
+ except Exception as e:
162
+ print(f"Error: {e!s}")